JavaEE|【概念】锁策略, cas 和 synchronized 优化过程

目录
一 . 常见的锁策略
1. 乐观锁和悲观锁
2. 读写锁
3. 重量级锁 vs 轻量级锁
4 自旋锁
5.公平锁 vs 非公平锁
6 可重入锁 vs 不可重入锁
二、CAS
1. 实现原理
2.CAS 有哪些应用
1) 实现原子类
2) 实现自旋锁
3 CAS 的 ABA 问题
三、Synchronized 原理
基本特点
优化过程:
1. 锁消除
2. 锁粗化
一 . 常见的锁策略
1. 乐观锁和悲观锁

悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做
2. 读写锁
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
注意:一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
  • 两个线程都要写一个数据, 有线程安全问题.
  • 一个线程读另外一个线程写, 也有线程安全问题.
读写锁的三种状态:
  • 读模式下加锁(读锁)
  • 写模式下加锁(写锁)
  • 不加锁模式下
ReentrantReadWriteLock.ReadLock //类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁. ReentrantReadWriteLock.WriteLock //类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁

3. 重量级锁 vs 轻量级锁 锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的。CPU 提供了 "原子操作指令";操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁;JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类。
重量级锁: 加锁机制重度依赖了 OS 提供了 mutex
  • 大量的内核态用户态切换
  • 很容易引发线程的调度
轻量级锁: 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.
  • 少量的内核态用户态切换.
  • 不太容易引发线程调度.
4 自旋锁
  • 自旋锁:是线程获取锁时不会立即阻塞,而是通过循环的方式去得到锁,这样做可以减少上下文的切换
  • 读自旋锁的缺点:缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的
5.公平锁 vs 非公平锁
公平锁: 遵守 "先来后到". B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 "先来后到". B 和 C 都有可能获取到锁
注意:
  • 操作系统内部的线程调度就可以视为是随机的。 如果不做任何额外的限制, 锁就是非公平锁。 如果要想实现公平锁, 就需要依赖额外的数据结构,来记录线程们的先后顺序.
  • 公平锁和非公平锁没有好坏之分, 关键还是看适用场景
synchronized就是非公平锁。
6 可重入锁 vs 不可重入锁
可重入锁是指可以重新进入的锁,一个线程针对一把锁,连续两次加锁不会出现死锁,这种就是可重入锁

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类。
synchronized关键字锁都是可重入的
二、CAS CAS 可以视为是一种乐观锁.
全称 Compare and swap, 即 "比较并交换". 相当于通过一个原子的操作, 同时完成 "读取内存, 比
较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的支撑.
1. 实现原理
  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
2.CAS 有哪些应用 1) 实现原子类
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
public class AtomiclntegerDemo { private static int number = 0; private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { for (int i = 0; i <=100000; i++) { //number++; atomicInteger.getAndIncrement(); //++i //AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作. } }); thread.start(); thread.join(); System.out.println("最终结果" + atomicInteger.get()); } }

2) 实现自旋锁
基于 CAS 实现更灵活的锁, 获取到更多的控制权
public class SpinLock { private Thread owner = null; public void lock(){ // 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){} } public void unlock (){ this.owner = null; } }

3 CAS 的 ABA 问题
什么是ABA 的问题:
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
先读取 num 的值, 记录到 oldNum 变量中.
使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A
解决方案:
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
1. CAS 操作在读取旧值的同时, 也要读取版本号.
2. 真正修改的时候:如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败。
三、Synchronized 原理
package sync; public class 语法 {//语法二:同步实例方法 public synchronized void t1(){} //等同于: this是指 // 当前线程(谁(当前类的某个实例对象)调用我(实例方法),this就是谁) public void t1_equals(){ synchronized(this){} }//语法三:静态同步方法 public static synchronized void t2(){ //代码行: } //等同于 public static void t2_equals(){ synchronized(语法.class){ //代码行 } }//类加载:还会在堆中,生成一个类对象 public static void main(String[] args) { Class c = String.class; //执行当前类的类加载:在堆中,会生成一个语法.class的类对象 System.out.println(语法.class == 语法.class); //以上代码等同于 Class<语法> c1 = 语法.class; Class<语法> c2 = 语法.class; System.out.println(c1 == c2); //synchronized语法一:同步代码块 Object 某个对象 = new Object(); synchronized (某个对象){} //当然也可以使用类对象 synchronized (语法.class){} } }

基本特点 结合上面的锁策略, 我们就可以总结出, Synchronized 具有以下特性(只考虑 JDK 1.8):
  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁
优化过程: 1. 锁消除
编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
package sync优化; public class 锁消除 {public static void main(String[] args) { //局部变量只有当前方法执行的线程持有(不可能有其他线程持有) //也就不存再线程安全问题:jvm给append中synchronized加锁释放锁 // 优化方案,就是“锁消除”=>不加锁 StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c"); System.out.println(sb.toString()); } }

此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销。这就来到了锁粗化。
2. 锁粗化
一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化
for(int i = 0; i < 10000; i++) { synchronized (synchronized线程安全.class) { count++; } }synchronized (synchronized线程安全.class) { //加锁 for(int i = 0; i < 10000; i++) { count++; } }// 释放锁public static void main(String[] args) { StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c"); //其中连续三次同时append,加锁---释放锁。 System.out.println(sb.toString()); }

【JavaEE|【概念】锁策略, cas 和 synchronized 优化过程】

    推荐阅读