十分钟搞定大厂java面试必问并发的基石CAS

  • 作者: 凯哥Java(公众号:凯哥Java)
  • 并发
  • 时间:2020-03-14 12:46
  • 1438人已阅读
简介 Cas也是线程同步的一种解决方案,很多人区分不清楚它和synchronized。先看下这段代码:public class MyCas {    private static int num = 0;    public stati

🔔🔔🔔好消息!好消息!🔔🔔🔔

有需要的朋友👉:联系凯哥 微信号 kaigejava2022

Cas也是线程同步的一种解决方案,很多人区分不清楚它和synchronized。

先看下这段代码:

public class MyCas {
    private static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i1 = 0; i1 < 1000; i1++) {
                        num++;
                    }
                }
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(num);
    }}

创建了两个线程每个线程对num进行了1000次加1操作,打印出来的结果是多少呢?

了解线程同步的同学应该知道这段代码是线程不安全的结果不一定是2000,而是小于等于2000。

如果想让它变得线程安全可以在num++这里加上“synchronized”关键字:

for(int i1=0;i1< 1000;i1++){synchronized (MyCas.class){
        num++;
        }}

问题是解决了,但是我们知道加了synchronized关键字,一次只允许一个线程执行num++的代码,其它线程会被阻塞,之后被唤醒,比较影响性能。

有没有其它方法来解决这个问题呢?

public class MyCas {
    private static AtomicInteger num = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i1 = 0; i1 < 1000; i1++) {
                        synchronized (MyCas.class) {
                            num.incrementAndGet();
                        }
                    }
                }
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(num);
    }}

这段代码也能保证最后输出的结果为2000.

这里我们用到了一个类AtomicInteger,通过调用它的incrementAndGet()方法完成加一的操作,AtomicInteger到底是个什么鬼,这么神奇..

public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value;

    public final int incrementAndGet() {
        for (; ; ) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

    public final int get() {
        return value;
    }}

重点看一下incrementAndGet()方法,他的功能是进行加一的操作,里面是一个无限循环,其它的代码都很容易理解,就是进行加一的操作.

似乎并没有进行同步,看来玄机就在这个 compareAndSet()方法了,我们来一探究竟:

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

compareAndSet方法里面只有一行代码:

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

正是这行代码实现了线程安全。

在深入这行代码之前先要给大家介绍一个概念就是我们今天的主题cas,其实cas就是compareAndSwap取每个单词的第一个字母的简称

什么是cas?他是如何做到线程同步的?

num++相当于num=num+1 执行这行代码在计算机中分为三步:

1. 将num的值从内存中取出

2. 将num的值加1

3. 将增加过后的num保存回内存

在多线程的情况下当一个线程获取了num的值1,另一个线程同时也获取了num的值,并且加一,将num的值保存回去,num变成了2,第一个线程完成加一的操作后把num的值保存回去,此时num的值还是2,而不是我们希望看到的3.

看看cas机制是如何处理的。

1. 将num的值从内存中取出

2. 将num的值加一

3. 用第一步中取出的num值和内存中的num值比较如果相同说明num值没有被修改,则将第二步计算出来的num值存入内存,如果不相同说明num值已被修改,重复步骤1,2,3

问题得到了解决,是不是很机智。

看到这里,有的朋友会产生疑问,第三个步骤分成了两步,比较和存值,本身就是线程不安全啊,实际上比较和存值在cpu的层面上是一个指令,是线程安全的

明白了cas我们再回过头来看这行代码:

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

这里的valueOffset相当于要修改的变量在内存中的地址expect是要修改的变量原来的值,update是该变量修改以后的值unsafe.compareAndSwapInt()方法通过这几个参数实现了cas,这里的unsafe是Unsafe类型的对象,它给我们提供了硬件级别的原子操作。

Cas也不是完美无缺的

1. 在并发量大的情况下,如果线程反复尝试更新变量却不能成功,导致不断循环会给cpu造成很大的压力

2. Cas只能同步一个变量,而不能同步一段代码,如果要对多个变量同时进行更新cas就无能为力了。

3. Cas会造成ABA问题。

什么是ABA?

1. 线程一,从内存中取出num的值A

2. 线程二将num的值改为B

3. 线程二将num的值改为A

4. 此时线程一处理完毕,想将num的值保存回内存,将第一步取出来的num值和内存中的值比较,发现一样,保存num值,结束!

果真没问题吗?

这里num的值其实是发生了变化,只是又被改了回来,所以看上去没有变化,然而这种情况下可能会出现问题。

怎么解决呢?

通过加入版本号就可以解决:

1线程一,从内存中取出num的值A版本号为01

2. 线程二将num的值改为B版本号为02

3. 线程二将num的值改为A版本号为03

4. 此时线程一处理完毕,想将num的值保存回内存,将第一步取出来的num值和内存中的值比较,发现一样,比较版本号发现不一样,保存失败!

总结一下:

1. cas通过Unsafe提供的底层指令来实现比较并交换

2. cas并不是万能的,在并发操作多的情况下,效率较低

3. cas只能实现单个变量的同步

4. 解决ABA问题可以通过加入版本号来实现


TopTop