Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

时间:2024-01-14 14:58:50

精彩理解:  https://www.jianshu.com/p/21be831e851e ;  https://blog.csdn.net/heyutao007/article/details/19975665 ;

备选参考:https://blog.csdn.net/tanga842428/article/details/52742698; https://www.cnblogs.com/yitong0768/p/4555445.html ;

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。CAS的原子性是由 CPU的CPI硬件指令实现保证的,即调用native方法调用由C++编写的硬件级别指令,jdk中提供Unsafe类执行这些操作。

一.直入正题

现在有这么一个需求,需要实现一个支持并发的计数功能,例如下面的代码

public class Increment{
  private int count=0;
  //多线程环境下对count执行++操作
}

面试官可能会提供上述代码问你,你觉得这个操作有什么问题,如果你说没有问题,那么恭喜你,大兄嘚,你可以走了,面试到此结束!

如果你说这段代码有问题,因为在并发环境下对count进行自增运算是不安全,好了,兄嘚,你入坑了。接着面试官会问问什么不安全以及如何解决这个问题呢?

二.为什么并发环境下的count自增操作不安全

因为count++不是原子操作,而是三个原子操作的组合:

  1. 读取内存中的count值赋值给局部变量temp
  2. 执行temp+1操作
  3. 将temp赋值给count

所以如果两个线程同时执行count++操作的话,我们不能保证线程1按顺序执行完上述三步后线程2才开始执行。

面试官露出了满(yin)意(xian)的笑容,兄嘚你知道的太多了,好吧,咱们继续,既然你说这么操作是不安全的,那么怎么解决呢?

三.并发环境下count++不安全问题的解决方案

  1. synchronized加锁
public class Increment{
  private int count=0;
  public synchronized void add(){
    count++
  }
}

加锁后代码变成上面这样了,好了现在就是线程安全的了,同一时间只有一个线程能加锁,其他线程需要等待锁,这样就不会出现count计数不准确的问题了。

面试官还是不想放过你,接着问,这个方案还有没有可能优化一下呢?

引入synchronized会造成多个线程排队的问题,所以同一时间只有一个线程执行,这样的锁有点儿“重量级”了

虽然随着Java版本更新,也对synchronized做了很多优化,但是处理这种简单的累加操作,仍然显得“太重了”。人家synchronized是可以解决更加复杂的并发编程场景和问题的。

而且,在这个场景下,你要是用synchronized,不就相当于让各个线程串行化了么?一个接一个的排队,加锁,处理数据,释放锁,下一个再进来。

2.高效的方案Atomic原子类

你思索片刻说:对于这种的count++类的操作,我们完全可以换一种做法,java并发包下面提供了一系列的Atomic原子类,比如说AtomicInteger

public class Increment{
  private AtomicInteger count=new AtomicInteger();
  public synchronized void add(){
    count.incrementAndGet();
  }
}

多个线程可以并发的执行AtomicInteger的incrementAndGet()方法,意思就是给我把count的值累加1,接着返回累加后最新的值,实际上,Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

你心想:看到没我都知道,这种事情难不倒我

Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

这时面试官喝了口水说:小伙子懂得真多

你想这次面试应该问完了吧,可是没想到面试官接着又抛出一个问题:CAS底层实现原理是什么?

靠,没完了是吧,还问底层原理,也怪自己多嘴,非得说什么CAS,没办法,自己挖的坑说什么也得填上啊

流程图如下:

Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

假如说有3个线程并发的要修改一个AtomicInteger的值,他们底层的机制如下:

首先,每个线程都会先获取当前的值,接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。

然后CAS操作里,会比较一下,现在你的值是不是刚才我获取到的那个值啊?

如果是的话,OK!说明没人改过这个值,那你给我设置成累加1之后的一个值!

同理,如果有人在执行CAS的时候,发现自己之前获取的值跟当前的值不一样,会导致CAS失败,失败之后,进入一个无限循环,再次获取值,接着执行CAS操作!

说完连自己都佩服自己了

Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

心想可以了吧,结果面试官又抛出一个问题:CAS有没有什么问题?

3.CAS性能优化

真是要了亲命了,还有完没完啊,我想静静了!!!!

这个CAS有什么问题呢?从上面的流程图其实可以看出来,比如说大量的线程同时并发修改一个AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。

这些线程不停地获取值,然后发起CAS操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起CAS操作又失败了,再次进入下一个循环。

在大量线程高并发更新AtomicInteger的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好,这个问题说到这里,恭喜你,你已经击败了80%的对手了大兄嘚!!!那么如何优化呢?

Java 8有一个新的类,LongAdder,他就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能,这个类具体是如何优化性能的呢?咱们看图说话:

Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

LongAdder核心思想就是热点分离,这一点和ConcurrentHashMap的设计思想。就是将value值分离成一个数组,当多线程访问时,通过hash算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度

LongAddr的兄弟类如下:

Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

说到这里面试官终于说话:行,这个问题就问道这里了!!!

转自:https://www.toutiao.com/a6660643231585272331/?tt_from=c&iid=59710992155&app=news_article×tamp%3D1550844890&group_id=6660643231585272331