乐观锁的具体实现细节,主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS。

简述

CAS(compare and swap/set):当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程不会被挂起,而是告知这次竞争中失败,并可以再次尝试。

CAS操作中包含三个操作数:

  • 1、需要读写的内存位置(V)
  • 2、进行比较的预期原值(A)
  • 3、拟写入的新值(B)

如果内存位置V的值与预期原值A匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何处理。

无论哪种情况,它都会在CAS指令之前,返回该位置的值(在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值)。

CAS有效的说明了“我认为位置V应该包含值A;如果包含该值,那么将B放到这个位置;否则不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

java.util.concurrent就是建立在CAS之上的。相比于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。性能有了大幅提升。

在没有锁的机制下,字段value要借助volatile原语,保证线程间的数据是可见性。

CAS缺点

ABA问题

问题描述:

链表的相互替换修改后,会出现丢失前驱或后继的问题。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。如下所示:
image.png
现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:
head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:
image.png
此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:
image.png
其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

解决方案:

Java1.5开始,JDB的atomit包里面提供了一个AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。(即通过版本号确定当前数据版本是否所需要的版本)。

循环时间长,开销大

自旋CAS(不成功,就一直循环执行,直到成功)。如果长时间不成功,会给cpu带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。

pause指令有两个作用:

  • 1、可以延迟流水线执行命指令,在一些处理器上延迟时间是零。
  • 2、可以避免在退出循环的时候因内存顺序冲突而引起的CPU流水线被清空,从而提高CPU的执行效率。

    只能保证一个共享变量的原子操作

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

这个时候就可以用锁,或者把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用cas操作ij。
**

从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

CAS与Synchronized的使用场景

对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换一级用户态内核态间的切换操作额外浪费消耗CPU资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋的几率较少,因此可以获得更高的性能。

对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronezed。

补充:synchronized在jdk1.6之后,已经优化,synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

concurrent包的实现:
由于Java的CAS同时具有volatile读和写的内存语句,因此Java线程之间的通信现在有了下面四种方式:
1、A线程写volatile变量,随后B线程读这个volatile变量。
2、A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
3、A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
4、A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
此处可参考文章《Volitile原理剖析》
Volitile原理剖析