今天花了点时间了解了一下JDK1.8
中ConcurrentHashMap
的实现,发现它实现的主要思想就是依赖于CAS
机制。CAS
机制是并发中比较重要的一个概念,因此今天这篇博客就来详细介绍一下CAS
机制以及Java
中对CAS
的适用。java
在讲CAS
以前,先来理解两个概念,即乐观锁和悲观锁:并发
- 乐观锁:在并发下对数据进行修改时保持乐观的态度,认为在本身修改数据的过程当中,其余线程不会对同一个数据进行修改,因此不对数据加锁,可是会在最终更新数据前,判断一下这个数据有没有被修改,若没有被修改,才将它更新为本身修改的值;
- 悲观锁:在并发下对数据进行修改时保持悲观的态度,认为在本身修改数据的过程当中,其余线程也会对数据进行修改,因此在操做前会对数据加锁,在操做完成后才将锁释放,而在释放锁以前,其余线程没法操做数据;
CAS
其实就是乐观锁的一种实现方式,而悲观锁比较典型的就是Java
中的synchronized
。下面我就来详细介绍一下CAS
的相关概念。this
CAS
全称compare and swap
——比较并替换,它是并发条件下修改数据的一种机制,包含三个操做数:atom
- 须要修改的数据的内存地址(V);
- 对这个数据的旧预期值(A);
- 须要将它修改成的值(B);
CAS的操做步骤以下:线程
- 修改前记录数据的内存地址V;
- 读取数据的当前的值,记录为A;
- 修改数据的值变为B;
- 查看地址V下的值是否仍然为A,若为A,则用B替换它;若地址V下的值不为A,表示在本身修改的过程当中,其余的线程对数据进行了修改,则不更新变量的值,而是从新从步骤2开始执行,这被称为自旋;
经过以上四个步骤对内存中的数据进行修改,就能够保证数据修改的原子性。CAS
是乐观锁的一种实现,因此这里介绍的步骤和乐观锁的定义差很少,仍是很好理解的。code
Java
中大量使用的CAS
,好比,在java.util.concurrent.atomic
包下有不少的原子类,如AtomicInteger
、AtomicBoolean
......这些类提供对int
、boolean
等类型的原子操做,而底层就是经过CAS
机制实现的。好比AtomicInteger
类有一个实例方法,叫作incrementAndGet
,这个方法就是将AtomicInteger
对象记录的值+1
并返回,与i++
相似。可是这是一个原子操做,不会像i++
同样,存在线程不一致问题,由于i++
不是原子操做。好比以下代码,最终必定可以保证num
的值为200
:对象
// 声明一个AtomicInteger对象 AtomicInteger num = new AtomicInteger(0); // 线程1 new Thread(()->{ for (int i = 0; i < 100; i++) { // num++ num.incrementAndGet(); } }).start(); // 线程2 new Thread(()->{ for (int i = 0; i < 100; i++) { // num++ num.incrementAndGet(); } }).start(); Thread.sleep(1000); System.out.println(num);
咱们看看incrementAndGet
方法的源码:内存
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
这里使用了一个unsafe
对象,而unsafe
对象是什么呢?咱们知道,Java
并不能像C或C++
同样,直接操做内存,可是JVM
为咱们提供了一个后门,就是sun.misc.Unsafe
类,这个类为咱们实现了不少硬件级别的原子方法,固然,这些方法都是native
方法,使用其余语言实现,而不是Java
方法。而上面的另一个变量valueOffset
就是咱们须要修改的变量在内存中的偏移量。也许上面这个方法并不能让你感受使用了CAS
,那再看看下面这个方法:rem
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
compareAndSet
是AtomicInteger
的另外一个方法,它的做用就是给定一个预期的旧值expect
,以及须要更新为的值update
,若当前变量的值是expect
,就将其修改成update
,不然不修改(这不就是CAS的思想吗)。而它底层调用了unsafe
对象的compareAndSwapInt
方法,从这个名字能够看出,它的实现使用的就是CAS
。compareAndSwapInt
的三个参数valueOffset
、expect
以及update
,恰好对应了CAS
操做的三个操做数。get
CAS
机制虽然简单,可是也存在一些缺陷,其中比较典型的就是ABA
问题。什么是ABA
问题,我简单介绍一下:
T1
、T2
和T3
,它们都要对一个变量num
的值进行修改,且使用的都是CAS
机制进行同步,假设num
的初始值为100
;T1
首先读取了num
的值,将它记录为旧预期A1 = 100
,而后它想要将num
的值修改成80
,记录B2 = 80
,在执行num = B2
前,线程发生了切换,切换到线程T2
;T2
毫无阻碍地修改了num
的值,将它从100
修改成80
,而后线程再度切换,T3
开始执行;T3
也是毫无阻碍地修改了num
,将它从80
从新修改成100
,线程再次切换回T1
;T1
从上次运行的断点恢复,也就是准备用B1
的值覆盖num
,可是因为CAS
机制,它须要先检测num
的值是否等于它记录的预期值A1
,而后它发现A1 = num = 100
,认为num
没有被修改过,因而用B1
覆盖了num
; 上面这种状况就是CAS
的ABA
问题:一个变量被修改,可是又被改了回去,在CAS
机制中,将没法察觉这种错误的现象。在线程T1
被中断的过程当中,num
的值被修改,按照CAS
的原则,T1
应该放弃对num
的修改,从头开始执行。有人可能想问,修改回去以后,不就和没修改同样吗,有什么影响呢?乍一看确实如此,可是咱们考虑实际的应用场景,就会发现有些状况下会出现问题,举个简单的例子:
小明去银行取款,它的信用卡中有100元,他想要取出20,可是因为系统异常,系统发起了两个取款线程,一个对应上面的线程T1,一个对应线程T2,发生了和上面T一、T2如出一辙的状况。假设在T1中断的过程当中,小明的妈妈正好给他汇款20元,将T2修改的数据又复原了,钱又变回了100,对应上面的线程T3,此时将发生什么状况?线程T1和T2都会进行取钱操做,将100变成80,实际上总共扣了40元。可是,正确的状况应该是,T1检测到钱被修改过,就放弃修改,这样才不会形成错误。
对于ABA
问题的解决方案也很是简单,那就是再添加一个变量——版本号。每一个变量都加上一个版本号,在它被修改时,也同步修改版本号,而CAS
操做在修改前记录版本号,若在最后更新变量时,记录的版本号与当前版本号一致,表示没有被修改,可直接更新。
(1)优势
前面也提到过,CAS
是一种乐观锁,其优势就是不须要加锁就能进行原子操做;
(2)缺点
CAS
的缺点主有两点:
CAS
机制只能用在对某一个变量进行原子操做,没法用来保证多个变量或语句的原子性(synchronized
能够);(3)适用场景
由上面分析的优缺点能够看出,CAS
适用于并发冲突发生频率较低的场合,而对于并发冲突较频繁的场合,CAS
因为不断重试,反倒会下降效率。
CAS
是一种在并发下实现原子操做的机制,可是只能用来保证一个变量的原子性,适用于并发冲突频率较低的场合。
推荐两篇描述CAS的博客,这两篇博客经过漫画对CAS进行了很是详细的描述: