赞
踩
微信搜索【程序员囧辉】,关注这个坚持分享技术干货的程序员。
我的最新文章:BAT 老兵的经验之谈,成长路上这个道理越早知道越好
目录
CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS也是现在面试经常问的问题,本文将深入的介绍CAS的原理。
介绍CAS之前,我们先来看一个例子。
- /**
- * @author joonwhee
- * @date 2019/7/6
- */
- public class VolatileTest {
-
- public static volatile int race = 0;
-
- private static final int THREADS_COUNT = 20;
-
- public static void increase() {
- race++;
- }
-
- public static void main(String[] args) throws InterruptedException {
- Thread[] threads = new Thread[THREADS_COUNT];
- for (int i = 0; i < THREADS_COUNT; i++) {
- threads[i] = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < 10000; i++) {
- increase();
- }
- }
- });
- threads[i].start();
- }
-
- while (Thread.activeCount() > 1) {
- Thread.yield();
- }
- System.out.println(race);
- }
- }
这个例子有些网友反馈会进入死循环,我后面也发现了,在IDEA的RUN模式下确实会陷入死循环,通过 Thread.currentThread().getThreadGroup().list(); 代码可以打印出当前的线程情况如下:
- java.lang.ThreadGroup[name=main,maxpri=10]
- Thread[main,5,main]
- Thread[Monitor Ctrl-Break,5,main]
可以看到,除了Main方法线程后,还有一个Monitor Ctrl-Break线程,这个线程是IDEA用来监控Ctrl-Break中断信号的线程。
解决死循环的办法:如果是IDEA,可以使用DEBUG模式运行就可以,或者使用下面这段代码。
- import java.util.concurrent.CountDownLatch;
-
- /**
- * @author joonwhee
- * @date 2019/7/6
- */
- public class VolatileTest {
-
- public static volatile int race = 0;
-
- private static final int THREADS_COUNT = 20;
-
- private static CountDownLatch countDownLatch = new CountDownLatch(THREADS_COUNT);
-
- public static void increase() {
- race++;
- }
-
- public static void main(String[] args) throws InterruptedException {
- Thread[] threads = new Thread[THREADS_COUNT];
- for (int i = 0; i < THREADS_COUNT; i++) {
- threads[i] = new Thread(new Runnable() {
- @Override
- public void run() {
- for (int i = 0; i < 10000; i++) {
- increase();
- }
- countDownLatch.countDown();
- }
- });
- threads[i].start();
- }
- countDownLatch.await();
- System.out.println(race);
- }
- }
上面这个例子在volatile关键字详解文中用过,我们知道,运行完这段代码之后,并不会获得期望的结果,而且会发现每次运行程序,输出的结果都不一样,都是一个小于200000的数字。
通过分析字节码我们知道,这是因为volatile只能保证可见性,无法保证原子性,而自增操作并不是一个原子操作(如下图所示),在并发的情况下,putstatic指令可能把较小的race值同步回主内存之中,导致我们每次都无法获得想要的结果。那么,应该怎么解决这个问题了?
解决方法:
首先我们想到的是用synchronized来修饰increase方法。
使用synchronized修饰后,increase方法变成了一个原子操作,因此是肯定能得到正确的结果。但是,我们知道,每次自增都进行加锁,性能可能会稍微差了点,有更好的方案吗?
答案当然是有的,这个时候我们可以使用Java并发包原子操作类(Atomic开头),例如以下代码。
我们将例子中的代码稍做修改:race改成使用AtomicInteger定义,“race++”改成使用“race.getAndIncrement()”,AtomicInteger.getAndIncrement()是原子操作,因此我们可以确保每次都可以获得正确的结果,并且在性能上有不错的提升(针对本例子,在JDK1.8.0_151下运行)。
通过方法调用,我们可以发现,getAndIncrement方法调用getAndAddInt方法,最后调用的是compareAndSwapInt方法,即本文的主角CAS,接下来我们开始介绍CAS。
getAndAddInt方法解析:拿到内存位置的最新值v,使用CAS尝试修将内存位置的值修改为目标值v+delta,如果修改失败,则获取该内存位置的新值v,然后继续尝试,直至修改成功。
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
上面源码分析时,提到最后调用了compareAndSwapInt方法,接着继续深入探讨该方法,该方法在Unsafe中对应的源码如下。
可以看到调用了“Atomic::cmpxchg”方法,“Atomic::cmpxchg”方法在linux_x86和windows_x86的实现如下。
linux_x86的实现:
windows_x86的实现:
Atomic::cmpxchg方法解析:
mp是“os::is_MP()”的返回结果,“os::is_MP()”是一个内联函数,用来判断当前系统是否为多处理器。
LOCK_IF_MP(mp)会根据mp的值来决定是否为cmpxchg指令添加lock前缀。
这是一种优化手段,认为单处理器的环境没有必要添加lock前缀,只有在多核情况下才会添加lock前缀,因为lock会导致性能下降。cmpxchg是汇编指令,作用是比较并交换操作数。
上面的第1点保证了CAS操作是一个原子操作,第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。
当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。但是我们可以通过以下两种办法来解决:1)使用互斥锁来保证原子性;2)将多个变量封装成对象,通过 AtomicReference 来保证原子性。
CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值 B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。
但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
另外,我还准备了很多大厂面试资料、0基础自学教程,由于不能放外链,所以有需要的小伙伴去公众号【程序员囧辉】回复【资料】自行获取好了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。