Java锁和CAS

53 阅读6分钟

synchronized的执行过程

  1. 线程抢锁时,JVM首先检测内置锁对象Mark Word中biased_lock(偏向锁标识)是否设置 成1,lock(锁标志位)是否为01,如果都满足,确认内置锁对象为可偏向状态。
  2. 在内置锁对象确认为可偏向状态之后,JVM检查Mark Word中线程ID是否为抢锁线程ID, 如果是,就表示抢锁线程处于偏向锁状态,抢锁线程快速获得锁,开始执行临界区代码。
  3. 如果Mark Word中线程ID并未指向抢锁线程,就通过CAS操作竞争锁。如果竞争成功,就 将Mark Word中线程ID设置为抢锁线程,偏向标志位设置为1,锁标志位设置为01,然后执行临界 区代码,此时内置锁对象处于偏向锁状态。
  4. 如果CAS操作竞争失败,就说明发生了竞争,撤销偏向锁,进而升级为轻量级锁。
  5. JVM使用CAS将锁对象的Mark Word替换为抢锁线程的锁记录指针,如果成功,抢锁线程 就获得锁。如果替换失败,就表示其他线程竞争锁,JVM尝试使用CAS自旋替换抢锁线程的锁记录 指针,如果自旋成功(抢锁成功),那么锁对象依然处于轻量级锁状态。
  6. 如果JVM的CAS替换锁记录指针自旋失败,轻量级锁膨胀为重量级锁,后面等待锁的线程 也要进入阻塞状态。

CAS

优势

  1. 属于无锁编程,线程不存在阻塞和唤醒这些重量级的操作。
  2. 进程不存在用户态和内核态之间的运行切换,进程不需要承担频繁切换的开销。

CAS 操作的弊端和规避措施

CAS 操作的弊端 CAS操作的弊端主要有以下4点。

(1)ABA问题

使用CAS操作内存数据时,当数据发生过变化也能更新成功,如操作序列A==>B==>A时,最 后一个CAS的预期数据A实际已经发生过更改,但也能更新成功,这就产生了ABA问题。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候将版 本号加1,那么操作序列A==>B==>A的就会变成A1==>B2==>A3,如果将A1当作A3的预期数据, 就会操作失败。

JDK提供了两个类AtomicStampedReference和AtomicMarkableReference来解决ABA问题。比较 常用的是AtomicStampedReference类,该类的compareAndSet方法的作用是首先检查当前引用是否等 于预期引用,以及当前印戳是否等于预期印戳,如果全部相等,就以原子方式将引用和印戳的值一 同设置为新的值。

(2)只能保证一个共享变量之间的原子性操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个 共享变量操作时,CAS无法保证操作的原子性。

一个比较简单的规避方法为:把多个共享变量合并成一个共享变量来操作。

JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个 AtomicReference实例后再进行CAS操作。比如有两个共享变量i=1、j=2,可以将二者合并成一个 对象,然后用CAS来操作该合并对象的AtomicReference引用。

(3)无效CAS会带来开销问题

自旋CAS如果长时间不成功(不成功就一直循环执行,直到成功为止),就会给CPU带来非 常大的执行开销。

(4)在部分CPU平台上存在“总线风暴”问题

CAS操作和volatile一样也需要CPU进行通过MESI协议各个内核的“Cache一致性”,会通过 CPU的BUS(总线)发送大量MESI协议相关的消息,产生“Cache一致性流量”。因为总线被设计 为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈,这就是所谓的“总线风暴”。

提升CAS性能

提升CAS性能有效方式之一是以空间换时间,分散竞争热点。较为常见的方案为:

1)分散操作热点,使用LongAdder替代基础原子类AtomicLong,LongAdder将单个CAS热点 (value值)分散到一个cells数组中。

2)使用队列削峰,将发生CAS争用的线程加入一个队列中排队,降低CAS争用的激烈程度。 JUC中非常重要的基础类AQS(抽象队列同步器)就是这么做的。

提升CAS性能有效方式之二是使用线程本地变量,从根本上避免竞争。

volatile

Java内存模型的规定如下: 1)所有变量存储在主存中。 2)每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的。 3)不同线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递。

volatile关键字的主要有两层语义:

  1. 不同线程对volatile变量的值具有内存可见性,即一个线程修改了某个volatile变量的值,该值对其他线程立即可见。
  2. 禁止进行指令重排序。用volatile修饰的变量在硬件层面上会通过在指令前后加入内存屏障来实现。

volatile能保证数据的可见性,但volatile不能完全保证数据的原子性,对于volatile类型的变量进行复合操作(如++)其仍存在线程不安全的问题。

AQS

AQS建立在CAS原子操作和volatile可见性变量的基础之上,为上层的显式锁、同步工具类、 阻塞队列、线程池、并发容器、Future异步工具提供线程之间同步的基础设施。所以,AQS在JUC 框架中的使用是非常广泛的。 image.png

ReentrantLock

  1. 非公平同步器ReentrantLock.NonfairSync的核心思想就是当前进程尝试获取锁的时候,如果发 现锁的状态位是0,就直接尝试将锁拿过来,然后执行setExclusiveOwnerThread(),根本不管同步队 列中的排队节点。
  2. 公平同步器ReentrantLock.FairSync的核心思想是通过AQS模板方法去进行队列入队操作。