悟空
种树比较好的时间是十年前,其次是如今。
自主开发了Java学习平台、PMP刷题小程序。目前主修Java
、多线程
、SpringBoot
、SpringCloud
、k8s
。
本公众号不限于分享技术,也会分享工具的使用、人生感悟、读书总结。java
夜黑风高的晚上,一名苦逼程序员正在疯狂敲着键盘,忽然他老婆带着一副睡眼朦胧的眼神瞟了下电脑桌面。因而有了以下对话:git
老婆:这画的图是啥意思,怎么还有三角形,四边形?程序员
我:我在画CAS的原理,要不我跟你讲一遍?github
老婆:好呀!小程序
案例:甲看见一个三角形积木,以为很差看,想替换成五边形,可是乙想把积木替换成四边形。(前提条件,只能被替换一次)安全
甲比较鸡贼,想到了一个办法:“我把积木带到另一个房间里面去替换,并上锁,就不会被别人打扰了。”(这里用到了排他锁synchronized
)markdown
乙以为甲太不厚道:“房间上了锁,我进不去,我也看不见积木长啥样。(因上了锁,因此不能访问)”多线程
因而甲、乙想到了另一个办法:谁先抢到积木,谁先替换,若是积木形状变了,则不容许其余人再次替换。(比较并替换CAS
)架构
因而他们就开始抢三角形积木:并发
场景1:甲抢到,替换成五边形,乙不能替换
乙后抢到,积木已经变为五边形了,乙就没机会替换了(由于甲、乙共一次替换机会)。
场景2:乙抢到未替换,甲替换成功
假如乙先抢到了,可是忽然以为三角形也挺好看的,没有替换,放下积木就走开了。
而后甲抢到了积木,积木仍是三角形的,想到乙没有替换,就把三角形替换成五边形了。
场景3:乙抢到,替换成三角形,甲替换成五边形,ABA问题
老婆听完后,以为这三种场景都太简单了,原来计算机这么简单,早知道我也去学计算机。。。
被无情鄙视了,好在老婆竟然听懂了,不知道你们听懂没?
回归正传,咱们用计算机术语来说下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
复制代码
咱们来对比看下原理图理解下上面代码的过程
图画得很是棒!
上述的场景和咱们用Git代码管理工具是同样的,若是有人先提交了代码到develop分支,另一我的想要改这个地方的代码,就得先pull develop分支,以避免提交时提示冲突。
这里咱们用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
还未执行自增操做。
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)
:根据当前对象var1和对象的内存偏移量var2获得主内存中变量的值,赋值给var5,并在main线程的工做内存中存放一份var5的副本(4)在362行打上断点,main线程继续执行一步
(5)切换到子线程aaa,仍是在361行断点处,还未获取主内存的值
(6)子线程aaa继续执行一步,获取到var5的值等于10
(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。
(8)切换到子线程aaa,进行比较并自增
由于此时aaa线程的var5=10,而主内存中的值已经更新为11了,因此比较后发现被其余线程修改了,不能进行替换,返回false,继续执行do while循环。
(10)子线程aaa继续执行,进行比较和替换,结果为true
因var5=11,主内存中的变量值也等于11,因此比较后相等,能够进行替换,替换值为var5+var4,结果为12,也就是自增1。退出循环,返回变量更新前的值var5=11。
至此,getAndIncrement方法的整个原子自增的逻辑就debug完了。因此能够得出结论:
先比较线程中的副本是否与主内存相等,相等则能够进行自增,并返回副本的值,若其余线程修改了主内存中的值,当前线程不能进行自增,须要从新获取主内存的值,而后再次判断是否与主内存中的值是否相等,以此往复。
不知道你们发现没,aaa线程可能会出现循环屡次的问题,由于其余线程可能将主内存的值又改了,可是aaa线程拿到的仍是老的数据,就会出现再循环一次,就会给CPU带来性能开销。这个就是自旋
。
频繁出现自旋,循环时间长,开销大
(由于执行的是do while,若是比较不成功一直在循环,最差的状况,就是某个线程一直取到的值和预期值都不同,这样就会无限循环)一个
共享变量的原子操做
一个
共享变量执行操做时,咱们能够经过循环CAS的方式来保证原子操做多个
共享变量操做时,循环CAS就没法保证操做的原子性,这个时候只能用锁来保证原子性本篇从和老婆的对话开始,以通俗的语言给老婆讲了CAS问题,其中还涉及到了并发锁。而后从底层代码一步一步debug,深刻理解了CAS的原理。
每一张图都力求精美!分享+在看啊,大佬们!
彩蛋: 还有一个ABA问题没有给你们讲,另外这里怎么不是AAB(拖拉机),AAA(金花)?
这周前三天写技术文章花了大量时间,少熬夜,睡觉啦 ~ 咱们下期再来说ABA问题,小伙伴们分享转发下好吗?您的支持是我写做最大的动力~
悟空,一只努力变强的码农!我要变身超级赛亚人啦!
另外能够搜索「悟空聊架构」或者PassJava666,一块儿进步!