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.youkuaiyun.com/v123411739/article/details/79561458
https://blog.youkuaiyun.com/Metis_/article/details/79380378
https://blog.youkuaiyun.com/qq_32998153/article/details/79529704

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值