CAS原理机制分析
一、CAS
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
二、案例分析
1.普通多线程count自增运算CASTest1.java
package com.cas;
public class CASTest1 {
public static int count = 0;
// 创建5个线程,在每个线程当中让count自增100次
// 由于线程不安全,所以这段代码最终的运行结果中count很有可能会小于500
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 100; j++) {
count++;
}
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
}
CASTest1,多次计算结果,有可能值等于500,也有可能值小于500,异常结果如下:
2.使用Synchronized同步锁,报障线程安全,count自增运算CASTest2.java
package com.cas;
public class CASTest2 {
public static int count = 0;
// 创建5个线程,在每个线程当中让count自增100次
// 使用Synchronized同步锁,报障线程安全,count最终结果等于500
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 100; j++) {
synchronized (CASTest2.class) {
count++;
}
}
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
}
CASTest2.java计算结果如下
3.使用Atomic操作类,提升性能,CASTest3.java
package com.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest3 {
public static AtomicInteger count = new AtomicInteger();
// 创建5个线程,在每个线程当中让count自增100次
// 使用AtomicInteger,Atomic操作类的底层正是用到了“CAS机制”,保障原子性操作,count最终结果等于500
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 每个线程当中让count值自增100次
for (int j = 0; j < 100; j++) {
//使用incrementAndGet方法进行自增运算
count.incrementAndGet();
}
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
}
CASTest3.java计算结果如下
三、CAS底层原理分析
1.CAS和Synchronized对比
Synchronized关键字会让没有得到锁资源的线程进入Blocked状态,而后在争夺到锁资源后恢复为Runing状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
另外,从思想上来说,Synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守。而CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
尽管JAVA 1.6之后为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然比较低(Synchronized转变为重量级锁之前,也会采用CAS机制)。所以面对这种情况,我们就可以使用Java中的原子操作类。
所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。而Atomic操作类的底层正是用到了“CAS机制”。
2.CAS的缺点
(1)CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
(2)不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
(3) ABA问题及解决方案
CAS 的使用流程通常如下:
1)首先从内存地址 V 读取值 A;
2)根据 A 计算目标值 B;
3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。
但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?
比如链表处理,出现ABA时可能会掉链,因为虽然链表头指针相同,但是链表的后续节点值有可能不同。
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。
Java并发包为了解决ABA问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
原子引用解决ABA问题代码案例
package com.zhang;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemoTest {
//原子引用类,解决ABA问题
static AtomicReference atomicReference = new AtomicReference(100);//初始值为100
static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);//初始值为100,版本号为1
public static void main(String[] args) {
System.out.println("以下是ABA问题的产生==========");
new Thread(() ->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"T1").start();
new Thread(() -> {
//暂停1秒,保证t1先执行,t2后执行
try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println("这里产生了ABA问题,发现值被改成了2021,CAS结果为:"+atomicReference.compareAndSet(100, 2021)+"\t"+atomicReference.get());
},"T2").start();
//暂停2秒
try { TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println("以下是ABA问题的解决==========");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//获取初始版本号
System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace();}
//4个参数分别为期望值、更新值、当前版本号、更新后版本号
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);//执行CAS操作并更新版本号
System.out.println(Thread.currentThread().getName() + "\t第2次版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);//执行CAS操作并更新版本号
System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());
},"T3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//获取初始版本号
System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);
try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();}
boolean result = atomicStampedReference.compareAndSet(100,2021,stamp,stamp+1);//乐观认为数据100没有被改变,尝试执行将数据100更新成2021操作
System.out.println(Thread.currentThread().getName()+"\t修改成功否:"+result+"\t当前最新版本号:"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t当前实际最新值:"+atomicStampedReference.getReference());
},"T4").start();
}
}
执行结果如下:
3.CAS的底层实现
(1)针对案例3中CASTest3.java的incrementAndGet()方法
通过方法调用,我们可以发现,getAndIncrement()方法调用getAndAddInt()方法,最后调用的是compareAndSwapInt()方法。如下两张图片来自网上博客getAndIncrement()方法。
getAndAddInt方法解析:拿到内存位置的最新值v,使用CAS尝试将内存位置的值修改为目标值v+delta,如果修改失败,则获取该内存位置的新值v,然后继续尝试(CAS的自旋操作),直至修改成功。
用volatile关键字来保证(保证线程间的可见性)获取的当前值是内存中的最新值。
(2)什么是unsafe呢?
Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。
我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。
正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
参考资料
https://blog.csdn.net/v123411739/article/details/79561458
https://blog.csdn.net/Metis\_/article/details/79380378
https://blog.csdn.net/qq\_32998153/article/details/79529704
还没有评论,来说两句吧...