精彩理解: 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++不是原子操作,而是三个原子操作的组合:
- 读取内存中的count值赋值给局部变量temp
- 执行temp+1操作
- 将temp赋值给count
所以如果两个线程同时执行count++操作的话,我们不能保证线程1按顺序执行完上述三步后线程2才开始执行。
面试官露出了满(yin)意(xian)的笑容,兄嘚你知道的太多了,好吧,咱们继续,既然你说这么操作是不安全的,那么怎么解决呢?
三.并发环境下count++不安全问题的解决方案
- 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机制保证多线程修改一个数值的安全性。
你心想:看到没我都知道,这种事情难不倒我
这时面试官喝了口水说:小伙子懂得真多
你想这次面试应该问完了吧,可是没想到面试官接着又抛出一个问题:CAS底层实现原理是什么?
靠,没完了是吧,还问底层原理,也怪自己多嘴,非得说什么CAS,没办法,自己挖的坑说什么也得填上啊
流程图如下:
假如说有3个线程并发的要修改一个AtomicInteger的值,他们底层的机制如下:
首先,每个线程都会先获取当前的值,接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。
然后CAS操作里,会比较一下,现在你的值是不是刚才我获取到的那个值啊?
如果是的话,OK!说明没人改过这个值,那你给我设置成累加1之后的一个值!
同理,如果有人在执行CAS的时候,发现自己之前获取的值跟当前的值不一样,会导致CAS失败,失败之后,进入一个无限循环,再次获取值,接着执行CAS操作!
说完连自己都佩服自己了
心想可以了吧,结果面试官又抛出一个问题:CAS有没有什么问题?
3.CAS性能优化
真是要了亲命了,还有完没完啊,我想静静了!!!!
这个CAS有什么问题呢?从上面的流程图其实可以看出来,比如说大量的线程同时并发修改一个AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。
这些线程不停地获取值,然后发起CAS操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起CAS操作又失败了,再次进入下一个循环。
在大量线程高并发更新AtomicInteger的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好,这个问题说到这里,恭喜你,你已经击败了80%的对手了大兄嘚!!!那么如何优化呢?
Java 8有一个新的类,LongAdder,他就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能,这个类具体是如何优化性能的呢?咱们看图说话:
LongAdder核心思想就是热点分离,这一点和ConcurrentHashMap的设计思想。就是将value值分离成一个数组,当多线程访问时,通过hash算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度
LongAddr的兄弟类如下:
说到这里面试官终于说话:行,这个问题就问道这里了!!!