多线程(进阶二:CAS)
目录
一、CAS的简单介绍
CAS的全称:“Compare And Swap”,字面意思是 “比较和交换”;一个CAS涉及到以下操作,有两个寄存器:A,SwapB,还有内存的值:AddressC。
先判断寄存器A是否和AddressC的值相同,相同,SwapB的值和AddressC的值进行交换,返回成功操作,否则返回失败操作。这里的交换值我们也可以理解成赋值操作,因为寄存器中的值我们不关心,用完就丢掉了,只关心内存中的值。
CAS逻辑(用伪代码来描述)
伪代码:
这里有两个寄存器:expectValue,swapValue,还有内存的值:address,初始化如图:
进入if语句,判断内存的值和寄存器e的值是否相等,如果相等,就交换寄存器swap和内存address的值,如图:
如图:
然后返回true,如果if条件不成立则返回false。
在计算机中,上述操作在计算机只是一条指令,因为单个指令的原因,所以CAS指令是原子的。
CAS指令不涉及到加锁,阻塞。基于CAS指令,合理使用的话,在多线程中,我们可以实现无锁编程;因为之前我们讨论并发编程的线程安全问题时,是通过加锁,阻塞这样方式解决线程安全问题,因为会有阻塞,所以性能也就会降低,用CAS指令实现无锁编程,也能保证线程安全,不涉及到阻塞,这样性能就能得到很大的提升,在多线程编程中打开了新世界的大门。
二、CAS在多线程中简单的使用
因为CAS是CPU的指令,有的cpu可能不支持CAS,但主流的CPU(x86,arm...)都是支持的。
CAS本身是CPU的指令,操作系统对其做了封装,jvm又对操作系统提供的api又做了一层封装。java中CAS的api是放在unsafe中的,这个包名的意思,顾名思义也是“不安全”的意思,一般在java中不建议使用,java的标准库中,对于CAS进行了进一步的封装,把CAS的一些操作封装成工具类,供程序猿使用。而主要的一个工具,叫做 “原子类”。
在 java.util.concurrent.atomic? 包下,里面的类就是基于上述方式实现的,典型的类就是AtomicInteger类,里面有很多方法可以实现数值的自增、自减,以及基本的加减操作。下面的代码案例也是使用AtomicInteger展示。
我们以前写过一个代码,让两个线程实现一个变量自增10_0000次,如下代码是线程不安全。
public class AtomicIntegerTest1 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
执行结果:
输出结果也和我们预期结果不同,肯定是线程不安全的,而原因就是因为count++操作不是原子的,在计算机中有三个指令。
这里,我们使用CAS的方式,来实现让两个线程实现一个变量自增10_0000次,代码如下
public class AtomicIntegerTest1 {
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();//和count++意思一样
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();//和count++意思一样
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
执行结果:
因为这里用CAS的方式,原本count++操作在计算机中有3条指令,不是原子的,肯定线程不安全;但是用CAS的方式就可以把像count++这样的操作,用一条指令完成,是原子的,在这里就不涉及到线程安全问题了。
AtomicInteger中有很多方法:自增,自减,+=,-=等待,这里就不展开讨论了。
三、原子类自增的代码分析
代码还是上述的代码,如下:
public class AtomicIntegerTest1 {
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();//和count++意思一样
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count.getAndIncrement();//和count++意思一样
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
在标准库中,getAndIncrement方法是这样的:
这里比较难理解,我们用伪代码的形式介绍,伪代码如下:
当有两个线程对同一个变量进行自增操作时,执行流程是这样的:
这里的oldValue是寄存器中的值,它的值就是AtomicInteger括号里面初始化的值,value是内存中的值。从上到下,把内存value的值都赋值给oldValue,两个线程拿到的都是同一个内存value的值。
这时,右边的线程while循环里的判断,先执行CAS这里的操作,如图:
????????
执行完CAS操作后返回true,然后while循环里true != ture,条件不成立,不执行while循环内的代码。
当左边线程进入while循环里面的判断语句时,如图:
也会进入CAS操作,这里因为内存value的值修改了,当前线程寄存器oldValue值还是0,给oldValue+1,会返回false,这时循环条件false != true成立,就会执行oldValue = value操作,如图:
再次进入循环条件里面执行CAS操作,这时候value == oldValue,所以会让oldvalue+1赋值给value,这时候value的值就是2了,如图:
所以,两个线程不管顺序是啥样的,使用getAndIncrement方法都不会出现线程安全问题,因为CAS操作本身就是原子的,原因的逻辑理解也大概是下面这样的:
如果cas不成功,会重复上面的操作,再次读取数据,这次读取到的数据就是正确的了,cas也就能成功。意思就是这个方法里面会判断拿到的值是不是最新值,如果不是就去拿最新的值,再去CAS,这时候因为拿到的是最新值,所以这时能CAS成功。
都看到这了,点个赞再走吧,谢谢谢谢谢
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!