19

程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?| 每一张图都力求精美

 4 years ago
source link: http://www.cnblogs.com/jackson0714/p/CAS.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
悟空

种树比较好的时间是十年前,其次是现在。

自主开发了Java学习平台、PMP刷题小程序。目前主修 Java多线程SpringBootSpringCloudk8s

本公众号不限于分享技术,也会分享工具的使用、人生感悟、读书总结。

夜黑风高的晚上,一名苦逼程序员正在疯狂敲着键盘,突然他老婆带着一副睡眼朦胧的眼神瞟了下电脑桌面。于是有了如下对话:

老婆:这画的图是啥意思,怎么还有三角形,四边形?

我:我在画CAS的原理,要不我跟你讲一遍?

老婆:好呀!

案例:甲看见一个三角形积木,觉得不好看,想替换成五边形,但是乙想把积木替换成四边形。(前提条件,只能被替换一次)

zaqaUbv.png!mobile

甲比较鸡贼,想到了一个办法:“我把积木带到另外一个房间里面去替换,并上锁,就不会被别人打扰了。”(这里用到了 排他锁synchronized

乙觉得甲太不厚道:“房间上了锁,我进不去,我也看不见积木长啥样。(因上了锁,所以不能访问)”

3AnIFv.png!mobile

于是甲、乙想到了另外一个办法: 谁先抢到积木,谁先替换,如果积木形状变了,则不允许其他人再次替换 。( 比较并替换CAS

于是他们就开始抢三角形积木:

  • 场景1: 甲抢到,替换成五边形,乙不能替换

    • 假如甲先抢到了,积木还是三角形的,就把三角形替换成五边形了。

    iay6na.png!mobile

    • 乙后抢到,积木已经变为五边形了,乙就没机会替换了(因为甲、乙共一次替换机会)。

      YNNRZzF.png!mobile

  • 场景2: 乙抢到未替换,甲替换成功

    • 假如乙先抢到了,但是突然觉得三角形也挺好看的,没有替换,放下积木就走开了。

    • 然后甲抢到了积木,积木还是三角形的,想到乙没有替换,就把三角形替换成五边形了。

    zuEJb2Y.png!mobile

  • 场景3: 乙抢到,替换成三角形,甲替换成五边形,ABA问题

    • 假如乙先抢到了,但是觉得这个三角形是旧的,就换了另外一个一摸一样的三角形,只是积木比较新。
    • 然后甲抢到了积木,积木还是三角形的,想到乙没有替换,就把三角形替换成五边形了。

nEVZBbr.png!mobile

老婆听完后,觉得这三种场景都太简单了, 原来计算机这么简单,早知道我也去学计算机 。。。

6nYneaZ.png!mobile

被无情鄙视了,好在老婆居然听懂了,不知道大家听懂没?

回归正传,我们用计算机术语来讲下Java CAS的原理

一、Java CAS简介

CAS的全称:Compare-And-Swap(比较并交换)。比较变量的现在值与之前的值是否一致,若一致则替换,否则不替换。

CAS的作用:原子性更新变量值,保证线程安全。

CAS指令:需要有三个操作数,变量的当前值(V),旧的预期值(A),准备设置的新值(B)。

CAS指令执行条件:当且仅当V=A时,处理器才会设置V=B,否则不执行更新。

CAS的返回指:V的之前值。

CAS处理过程:原子操作,执行期间不会被其他线程中断,线程安全。

CAS并发原语:体现在Java语言中sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用于范畴,是由 若干条指令 组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,所以CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,所以CAS是线程安全的。

二、能写几行代码说明下吗?

在上篇讲volatile时,讲到了如何使用原子整型类AtomicInteger来解决volatile的非原子性问题,保证多个线程执行num++的操作,最终执行的结果与单线程一致,输出结果为20000。

这次我们还是用AtomicInteger。

首先定义atomicInteger变量的初始值等于10,主内存中的值设置为10

AtomicInteger atomicInteger = new AtomicInteger(10);

然后调用atomicInteger的CAS方法,先比较当前变量atomicInteger的值是否是10,如果是,则将变量的值设置为20

atomicInteger.compareAndSet(10, 20);

设置成功,atomicInteger更新为20

当我们再次调用atomicInteger的CAS方法,先比较当前变量atomicInteger的值是否是10,如果是,则将变量的值设置为30

atomicInteger.compareAndSet(10, 30);

设置失败,因atomicInteger的当前值为20,而比较值是10, 所以比较后,不相等,故不能进行更新

完整代码如下:

package com.jackson0714.passjava.threads;
import java.util.concurrent.atomic.AtomicInteger;
/**
 演示CAS compareAndSet 比较并交换
 * @author: 悟空聊架构
 * @create: 2020-08-17
 */
public class CASDemo {
    public static void  main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(10);
        Boolean result1 = atomicInteger.compareAndSet(10,20);
        System.out.printf("当前atomicInteger变量的值:%d 比较结果%s\r\n", atomicInteger.get(), result1);
        Boolean result2 = atomicInteger.compareAndSet(10,30);
        System.out.printf("当前atomicInteger变量的值:%d, 比较结果%s\n" , atomicInteger.get(), result2);
    }
}

执行结果如下:

当前atomicInteger变量的值:20 比较结果true
当前atomicInteger变量的值:20, 比较结果false

INBFfyz.png!mobile

我们来对比看下原理图理解下上面代码的过程

  • 第一步:线程1和线程2都有主内存中变量的拷贝,值都等于10

zMRfAba.png!mobile

  • 第二步:线程1想要将值更新为20,先要将工作内存中的变量值与主内存中的变量进行比较,值都等于10,所以可以将主内存中的值替换成20

qe6ZZnE.png!mobile

  • 第三步:线程1将主内存中的值替换成20,并将线程1中的工作内存中的副本更新为20

IziymqF.png!mobile

  • 第四步:线程2想要将变量更新为30,先要将线程2的工作内存中的值与主内存进行比较10不等于20,所以不能更新

qyyEzaI.png!mobile

  • 第五步:线程2将工作内存的副本更新为与主内存一致:20

7ZrmAry.png!mobile

图画得非常棒!

iiyEZvz.png!mobile

上述的场景和我们用Git代码管理工具是一样的,如果有人先提交了代码到develop分支,另外一个人想要改这个地方的代码,就得先pull develop分支,以免提交时提示冲突。

三、能讲下CAS底层原理吗?

源码调试

这里我们用atomicInteger的getAndIncrement()方法来讲解,这个方法里面涉及到了比较并替换的原理。

示例如下:

public static void  main(String[] args) throws InterruptedException {
    AtomicInteger atomicInteger = new AtomicInteger(10);
    Thread.sleep(100);

    new Thread(() -> {
        atomicInteger.getAndIncrement();
    }, "aaa").start();

    atomicInteger.getAndIncrement();
}
  • (1)首先需要开启IDEA的多线程调试模式

  • (2)我们先打断点到17行,main线程执行到此行,子线程 aaa 还未执行自增操作。

neqyqee.png!mobile

getAndIncrement方法会调用unsafe的 getAndAddInt 方法,

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
  • (3)在源码 getAndAddInt 方法的361行打上断点,main线程先执行到361行

    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;
    }

    源码解释: 划重点!!!

    this.getIntVolatile(var1, var2)
    

nUbiUv2.png!mobile

  • (4)在362行打上断点,main线程继续执行一步

    • var5获取到主内存中的值为10

    BJviyuy.png!mobile

  • (5)切换到子线程aaa,还是在361行断点处,还未获取主内存的值

    RzuYRj3.png!mobile

  • (6)子线程aaa继续执行一步,获取到var5的值等于10

zEBZRr6.png!mobile

(7)切换到main线程,进行比较并替换

this.compareAndSwapInt(var1, var2, var5, var5 + var4)

var5=10,通过var1和var2获取到的值也是10,因为没有其他线程修改变量。compareAndSwapInt的源码我们后面再说。

所以比较后,发现变量没被其他线程修改,可以进行替换,替换值为var5+var4=11,变量值替换后为 11,也就是自增1。这行代码执行结果返回true(自增成功),退出do while循环。return值为变量更新前的值10。

RFF7Zbq.png!mobile

(8)切换到子线程aaa,进行比较并自增

因为此时aaa线程的var5=10,而主内存中的值已经更新为11了,所以比较后发现被其他线程修改了,不能进行替换,返回false,继续执行do while循环。

j2qq2yR.png!mobile

  • (9)子线程aaa继续执行,重新获取到的var=11

uQ3qM3f.png!mobile

  • (10)子线程aaa继续执行,进行比较和替换,结果为true

    因var5=11,主内存中的变量值也等于11,所以比较后相等,可以进行替换,替换值为var5+var4,结果为12,也就是自增1。退出循环,返回变量更新前的值var5=11。

buYJNfz.png!mobile

至此,getAndIncrement方法的整个原子自增的逻辑就debug完了。所以可以得出结论:

先比较线程中的副本是否与主内存相等,相等则可以进行自增,并返回副本的值,若其他线程修改了主内存中的值,当前线程不能进行自增,需要重新获取主内存的值,然后再次判断是否与主内存中的值是否相等,以此往复。

四、CAS有什么问题?

不知道大家发现没,aaa线程可能会出现循环多次的问题,因为其他线程可能将主内存的值又改了,但是aaa线程拿到的还是老的数据,就会出现再循环一次,就会给CPU带来性能开销。这个就是 自旋

  • 频繁出现自旋,循环时间长,开销大 (因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
  • 只能保证 一个 共享变量的原子操作
    一个
    多个
    
  • 引出来ABA问题(有彩蛋)

五、小结

本篇从和老婆的对话开始,以通俗的语言给老婆讲了CAS问题,其中还涉及到了并发锁。然后从底层代码一步一步debug,深入理解了CAS的原理。

每一张图都力求精美!分享+在看啊,大佬们!

彩蛋:还有一个ABA问题没有给大家讲,另外这里怎么不是AAB(拖拉机),AAA(金花)?

qqeMjmR.png!mobile

这周前三天写技术文章花了大量时间,少熬夜,睡觉啦 ~ 我们下期再来讲ABA问题,小伙伴们分享转发下好吗?您的支持是我写作最大的动力~

悟空,一只努力变强的码农!我要变身超级赛亚人啦!

VjyeEvJ.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK