50
CAS英文解释是比较和交换,是cpu底层的源语,是解决共享变量原子性实现方案,它定义了三个变量,内存地址值对应V,期待值E和要修改的值U,如下图所示,这些变量都是在高速缓存中的,如果两个线程A,B分别通过cas方式同时修改共享变量,假设当A线程先获取时间片,如果发现V的值和E相等就将主内存值更新为U,如果不相等说明线程B在线程A更新之前已经成功更新过,线程A会失败重试,此时根据缓存一致性协议,线程A的本地副本会失效,需要从主内存再同步最新的变量到本地内存副本,在Java中通过调用UnSafe的compareAndSet类似方式调用,底层是c,反编译后操作系统指令是cmpxchg指令。
image.png
保证i++原子性
你一定会有一个疑问,被volatile修饰的变量i,i++为什么会有线程安全问题呢,也就是原子性的问题,我们还是举一个经典的i++案例一步步分析吧!我们知道在多线程情况下volatile保证了共享变量的可见性,顺序行,但唯独不能保证原子性,原因是i++是一个复合操作,大致可以分成3步,1.先从主内存拿到最新的i值,2.将i加1这个操作保存到操作数栈,3.从栈中取出i加1的值写回到主内存。OK,当线程AB同时执行i++操作时,比如线程A先获取时间片,执行完第2步,这是线程A还未执行完,时间片分配给线程B,B顺利执行完所有操作后并同步了主内存,假设我们i的初始值是1,那么此时主内存值是2,因为线程B执行完毕,cpu时间片又回到线程A手上,做第3步操作,此时同步到主内存的值还是2,看,线程A,B各做了一次加1的操作,但最终结果可能是2,cas的作用就来了,他能保证i++操作的原子性,为什么能保证原子性呢?cas可以把上面三个操作合并成一个操作,是原子的。
有什么好处?
大家都知道解决多线程安全需要用到锁的,可以用synchronized来解决,但是synchronized也有它的劣势,最主要是它是阻塞的,阻塞会有什么问题?性能啊,这是计算机人不能忍的,频繁内核外核切换,会严重浪费系统资源,所以就提了cas这个乐观锁概念,它是非阻塞的,操作系统不用在内核态与用户态来回切换,相当于用while循环方式获取锁,在性能上有一定提升。即使这样,也会有一定问题,下面我们来看看。
有什么问题?
1.ABA问题。
这个案例比较简单,线程A把共享变量i,从1变成2,再变成1,线程B想把i变成2,本来应该是不会成功,因为即时变量i现在是1,但是它的状态变化了,他的解决方案是版本号。相当于修改成功一次版本号增加1,就可以解决了,曾经被面试官问到一个问题,cas是线程安全的吗?答案不是线程安全的。
2.自旋时间过长。
如果一个线程拿到锁后,一直不释放,其他线程就只能一直循环等待。
3.只能保证一个共享变量的原子性。
像Automic包下面的基本上都只能保证一个变量的原子性。
JUC包下面使用!
可能有些童鞋看JDK源码会比较纠结一个点,发现volatile关键字一般都会和cas连用,如果不要volatile会怎么样呢?cas本身只作用于方法,cas对共享变量没有约束,如果不对共享变量做volatile修饰,是不可见的,不能够保证共享变量的实效性,需要等待共享变量主动同步到主内存,这是需要花时间的,效率更低下,所有在JUC并发包里一直可以看到这样的volatile关键字一般都会和cas组合。
总结
这篇文章,我们先引出了cas概念,并且说明了它的优缺点,做了案例介绍,简单的和synchronized关键字做了比较,最后,深入的说明了volatile关键字和cas连用的效率,这是我在深入思考后得到的结论,分享给大家,文章有一定阅读门槛,如果有想搞清楚童鞋,可以1v1私聊讨论交流。希望大家喜欢。点赞哦!