Java多线程 (三)—— CAS机制

本文解析了CAS(Compare-And-Swap)机制的原理及其在Java并发编程中的应用,探讨了CAS为何能够保证操作的原子性,并分析了其优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、为什么需要CAS?

为什么需要CAS呢?我们知道java并发机制中主要有三个特性需要我们去考虑,原子性、可见性和有序性。比如我们使用volatile关键字修饰某一个变量,表明这个变量是全局共享的一个变量,保证了其可见性和有序性但却无法保证其原子性。我们先看下面一个例子:

public class ThreadCas {
    public static volatile int count = 0;
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(
                    new Runnable() {
                        public void run() {
                            try {
                                for (int j = 0; j < 10; j++) {//每个线程对count自增20
                                    System.out.println(count++);
                                    Thread.sleep(500);
                                }
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                    }).start();
        }
    }
}

上面例子我们使用5个线程对count进行增加,为了保证可见性和有序性还使用了volatile关键字对count进行修饰。正常我们预期的结果是50,但实际却不是,可以看下运行后的结果:
在这里插入图片描述
结果很明显,和想象的不一样,出现的两个46相同值,最终结果不是50。为什么会出现这个问题呢?这是因为变量count虽然保证了可见性和有序性,但是缺没有保证原子性。其原因我们可以来分析一下。
对于count++操作其实可以细分成三个步骤。
(1)从内存中读取count
(2)对count进行加1操作
(3)将count的值重新写入内存中
在单线程状态下上面的操作没有一点问题,但是在多线程中就会出现各种各样的问题了。因为可能一个线程对count进行了加1操作,还没来得及写入内存,其他的线程就读取了旧值,造成了线程的不安全现象。如何去解决这个问题呢?最常见的方式就是使用AtomicInteger来修饰count,AtomicInteger的作用就是为了保证原子性。下面我们用AtomicInteger重新测试下:

public class ThreadCas {
    public static AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(
                    new Runnable() {
                        public void run() {
                            try {
                                for (int j = 0; j < 10; j++) {//每个线程对count自增20
                                    System.out.println(count.incrementAndGet());
                                    Thread.sleep(500);
                                }
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                    }).start();
        }
    }
}

执行后结果如下:
在这里插入图片描述
上面的结果无论执行多少次,最终的值都是50。那么incrementAndGet是如何实现的呢?我们下面就来简单分析底层源码。

二、CAS底层原理

换句话说也就是CAS为什么能保证原子性?
(1)靠的是底层的Unsafe类
(2)Unsafe类是CAS的核心类,由于java无法直接访问底层系统,需要通过本地(native)方法访问,Unsafe相当于一个后门,该类可以直接操作特定的内存数据。 Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中的CAS依赖于Unsafe类中的方法
(3)注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,Unsafe类中的native方法是调用底层原语,原语是有原子性的。
下面让我们进入源码分析吧:

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

看到这里,我们可以发现底层使用的是sun.misc包下unsafe的getAndAddInt方法,但是现在我们还看不出什么,我们再深入到源码中看看getAndAddInt方法又是如何实现的

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

到了这一步就稍微有点眉目了,原来底层调用的是compareAndSwapInt方法,这个compareAndSwapInt方法其实就是CAS机制。因此如果我们想搞清楚AtomicInteger的原子操作是如何实现的,我们就必须要把CAS机制搞清楚,这也是为什么我们需要掌握CAS机制的原因。我们继续往下看compareAndSwapInt方法:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

我们可以看到这里面主要有四个参数,第一个参数就是我们操作的对象count,第二个参数是对象count的地址偏移量,第三个参数表示我们期待这个count是什么值,第四个参数表示的是count的实际值。
不过这里我们会发现这个compareAndSwapInt是一个native方法,也就是说再往下走就是C语言代码。这里就不继续深入研究了,有兴趣的同学可以通过openjdk继续往下研究底层C源码,会发现CAS真正实现机制由操作系统的汇编指令完成的。

三、CAS含义

CAS全称Compare-And-Swap(比较并交换),主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
1、变量内存地址,V表示
2、旧的预期值,A表示
3、准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新变量值,其他线程均会失败。失败线程会重新尝试或将线程挂起(阻塞)
我们使用一个例子来解释相信你会更加的清楚:

比如说给你儿子订婚。你儿子就是内存位置,你原本以为你儿子是和杨贵妃在一起了,结果在订婚的时候发现儿子身边是西施。这时候该怎么办呢?你一气之下不做任何操作。如果儿子身边是你预想的杨贵妃,你一看很开心就给他们订婚了,也叫作执行操作。现在你应该明白了吧。

四、CAS优点

cas是一种乐观锁,而且是一种非阻塞的轻量级的乐观锁,什么是非阻塞式的呢?其实就是一个线程想要获得锁,对方会给一个回应表示这个锁能不能获得。在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。

五、CAS缺点

缺点说明解决方法
ABA问题ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。具体什么场景下要解决CAS导致的ABA问题?如何解决?我会在下个章节中讲到。
举个例子解释说明下ABA:
你有一瓶水放在桌子上,别人把这瓶水喝完了,然后重新倒上去。你再去喝的时候发现水还是跟之前一样,就误以为是刚刚那杯水。如果你知道了真相,那是别人用过了你还会再用嘛?
每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。
Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳(stamp),从而避免ABA问题。
循环时间长开销大自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。
只能保证一个共享变量的原子操作CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。还可以考虑使用AtomicReference来包装多个变量,通过这种方式来处理多个共享变量的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值