初识CAS机制及其解决方案

1.不使用synchronized的Demo

首先我们写一下代码,开两个线程为静态变量自增到2000.

public class Test1 {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        for (int j = 0; j < 2; j++) {
            new Thread(()->{
                try {
                    Thread.sleep(10);
                    for (int i = 0; i < 1000; i++) {
                       System.out.println(count++);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        Thread.sleep(2000);
        System.out.println("count = "+count);
    }
}

由代码可知,此代码为非线程安全的,因此结果有时候为1999.

2.使用synchronized的Demo

接下来,我们给自增代码块引入synchronized关键字

public class Test1 {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        for (int j = 0; j < 2; j++) {
            new Thread(()->{
                try {
                    Thread.sleep(10);
                    for (int i = 0; i < 1000; i++) {
                        synchronized (Test1.class) {
                            System.out.println(count++);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        Thread.sleep(2000);
        System.out.println("count = "+count);
    }
}

加入同步锁之后,count自增操作变成了原子操作,因此count最后等于零,但引入synchronized来实现了线程安全,并不是一个最优的选择。

关键在于性能问题。

因为synchronized关键字让没有得到锁资源的线程进入Blocked状态(挂起),而争夺到锁的线程进入Runnable状态,该过程使得操作系统在用户模式和内核模式进行频繁的切换,代价比较高。

3.使用原子操作类的Demo

因此我们引入了原子操作类

原子操作类指的是JUC.atomic包下的一系列以Atomic开头的包装类。如AtomicBoolean,AtomicInteger,AtomicLong。他们用于对Boolean,Integer,Long类型进行的原子操作。

接下来我们尝试一下。

/**
 * @author Juniors
 * @date 2021/11/5 14:58
 */
public class Test2 {

    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(count.incrementAndGet());
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println("count="+count.get());
    }
}

因此使用AtomicInteger之后,最后的输出结果同样是200.在某些情况下,代码性能会比synchronized更好。

而Atomic操作类的底层正是用到了“CAS”机制。

4.引入CAS机制

CAS:CompareAndSwap 的缩写,即(先记录)再比较后替换。

CAS机制种使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

因此如果要更新变量的时候,只有当变量的预期值A和内存地址V中的实际值与之前记录的两值相同,才会将内存地址对应的值修改为B。


For example:

1.内存地址V当中,存储着值为10的变量。

在这里插入图片描述

2.此时线程1想把变量的值增加1,对线程1来说,记录的是当前的旧的预期值A=10,修改的新值B=11.

3.此时线程1将要提交的时候,被另一个线程2抢先一步,把内存地址V中的变量预期值率先更新成11.

在这里插入图片描述

4.此时线程1开始提交更新的时候,首先进行之前预期值A和地址V的实际值进行比较,结果发现A不等于V的实际值,因此提交失败。

5.线程1重新获取内存地址V和当前值A,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。因此重新尝试的过程被称为自旋


5.悲观锁与乐观锁

从思想上来说,synchronized属于悲观锁,悲观的认为程序的并发情况严重,需要严防死守,而CAS属于乐观锁,乐观的认为程序中的并发情况没那么严重,因此只需让线程不断的去重新更新(自旋)就行了。

6.CAS缺点

  • CPU 开销过大

    如果当前的并发量较高的话,就会有许多线程反复尝试更新某一个值,却一直不成功,循环往复,因此会给CPU带来很大压力

  • 不能保证代码块的原子性

    CAS机制所保证的只是一个变量的原子操作,而不能保证整个代码块的原子性。

  • ABA问题

    当A线程认为此时的预期值A为"A",但A线程更新值前的时候,已经有B,C两个线程已经进行了对预期值A的"B","A"两次更新操作,而A在进行预期值A和内存地址V进行比较时,误以为没有改变,其实变量已经改变,就出现了ABA问题。(A -》 B -》 A)

7.普通CAS的解决方案

接下来我们解决三个问题:

  1. Java中CAS的底层实现
  2. CAS操作多个变量的解决方案
  3. CAS的ABA问题及解决方案

7.1 Java中CAS的底层实现

下面是AtomicInteger中的常用incrementAndGet方法源码:

public final int incrementAndGet() {
    for (;;) {
        int current = get();

        int next = current + 1;

        if (compareAndSet(current, next))

            return next;

    }

}

private volatile int value; 

public final int get() {
    return value;

}

其中一段for的无限循环代码,也就是CAS的自旋,循环体中做了三件事情:

  1. 获取当前值
  2. 当前值+1,计算出目标值
  3. 进行CAS操作,如果成功跳出循环,否则失败继续循环自旋

我们注意到获取当前值用的是get()方法,即获取变量的当前值。

但我们如何获取当前值为内存中最新的值呢?So easy,用 volatile 关键字来保证(我们定义的变量在线程中可见性

接下来我们看一下compareAndSet方法:

        // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

	public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

这里涉及了两个重要对象,一个unsafe,一个valueOffset

其中unsafe是JVM来为我们提供间接访问底层的硬件级别的原子操作。

valueOffset代表的是Integer对象中的value成员变量在内存中的偏移量,也就是内存地址。

7.2 CAS操作多个变量的解决方案

当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。
但是我们可以通过以下两种办法来解决:
1)使用互斥锁来保证原子性;
2)将多个变量封装成对象,通过 AtomicReference 来保证原子性。

7.3 CAS的ABA问题及解决方案

接下来介绍一下我们如何解决ABA问题?

三个线程操作A,出现了A-》B-》A的状况。

该怎么解决呢?加个版本号就可以了。

真正要做到了谨慎的CAS机制,我们在compare阶段的时候不仅要将预期值A和内存地址V进行比较,而且还要比较变量的版本号是否一致。

在Java中,AtomicStampedReference类就实现了用版本号来做严谨CAS机制。
总结

1. java语言CAS底层如何实现?

利用unsafe提供的原子性操作方法。

2.什么事ABA问题?怎么解决?

当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。

利用版本号比较可以有效解决ABA问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值