浅谈CAS机制

先来看个实例:启动两个线程,每个线程中让变量countNum循环累加100次。

public class TestCas {
	private  static int countNum = 0;
//	private  volatile static AtomicInteger countNum = new AtomicInteger();
	public static void main(String[] args) {
		for(int i=0;i<2;i++) {
			new Thread(new Runnable() {
				public void run() {
					for(int j=0;j<100;j++) {
						countNum++;
//						synchronized (TestCas.class) {
//							countNum.incrementAndGet();
//						}
					}
				}
				
			}).start();;
		}
		/**
		 * 要想看到结果必须先让主线程让出CPU执行权,不然System.out.println(countNum)会先输出,看到的结果为0
		 */
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
		}
		System.out.println(countNum);
	}
}

多次运行结果都是小于200.因为自增操作是不安全的。首先我们可能想到的是给自增操作加入同步锁。

                        synchronized (TestCas.class) {
							countNum++;
                            //countNum.incrementAndGet();
						}

加了同步锁之后,最终的结果countNum一定是200但是在某些情况下,可能这种方案在性能上不是很好,因为:

synchronized关键字会让没有得到锁资源的线程进入BLOCKED(阻塞)状态,而后在争夺锁资源后回复为RUNNABLE(可运行)状态,这个过程中设计到操作系统用户模式内核模式的切换,代价比较高。虽然在java1.8位synchronized做了优化,增加了从偏向锁轻量级锁再到重量级锁的过渡,但是在最终转变为重量级锁之后,性能仍然比较低。那有什么替换方案吗?

有,使用Java中的原子操作类

Java中的原子操作类都在java.util.concurrent.atomic包下面,以Atomic开头的包装类,如:AtomicInteger,AtomicBoolean,AtomicLong,它们分别作用于Integer,Boolean,Long类型的原子性操作。

使用AtomicInteger改造上面程序:


	private  volatile static AtomicInteger countNum = new AtomicInteger();
                    for(int j=0;j<100;j++) {
							countNum.incrementAndGet();
					}

运行的结果为是200。

那Java这些原子性操作类的原理是什么呢?其实就是我们要将的CAS机制!

什么是CAS?

cas的全称是Compare and Swapd,即:比较并替换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新这个位置的值。

底层原理是:基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些原子操作类便是使用了这些封后的接口。

简单说:CAS机制当中使用了3个基本操作数,内存地址V,旧的预期值A,要修改的新值B。更改一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

举个例子加深理解:

1.内存地址V存储着值为10的变量。

2.此时线程1想要把变量的值增加1.对线程1来说,旧的预期值A=10,要更改的新值B=11.

3.在线程1要提交之前,另一个线程2抢先一步,把内存地址V中的变量率先更新为11.

4.此后线程1开始提交更新时,首先进行A和地址V的实际值比较(Compare),发现A不等V的实际值,提交失败。

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12,这个重新尝试的过程被称为自旋

6.这次比较幸运,没有被抢先执行,线程1进行比较(compare),发现A和地址V的实际是是相等的。

7.线程1进行替换(swap),b把地址V的值替换为B,也就是12.

从思想进行分类,synchronized属于悲观锁,悲观锁认为程序中的并发情况严重,必须一个一个来。CAS属于乐观锁,认为程序中的并发情况不严重,所以让线程不断去尝试更新。

其中从上面分析,CAS还是有缺点的,如:

    1.CPU开销较大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力

    2.不能保证代码块的原子性

 CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

     3.ABA问题

    举个例子分析:

    小明有100元存款,要用这个提款机来提款50元。

   由于提款机硬件出了点小问题,小明的提款操作被同时提交了两次,开启了两个线程,两个线程都是获取当前值100元(旧的预期值A),要更新成50(要替换的新值B)元。

   理想情况下,应该一个线程更新成功,另一个线程更新失败,小明的存款只被扣一次。

  首先线程1执行成功,把余额从100改成50.线程2因为某种原因阻塞了,这时候小明的妈妈刚好给小明汇款50元(线程3)。线程2仍然是阻塞状态,线程3执行成功,余额从50改成100。

线程2 恢复运行,由于阻塞之前已经获取了“旧的预期值”为100,并且经过Compare检测与现在地址V的实际值100相等,所以成功把变量值100更新成50了,余额变成50了。人生最痛苦的事情就是:人活着,钱没了,钱没了,钱没了。

这就是ABA问题

那有什么办法可以解决呢?

有,在Compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致

举个例子说明一下:

假设地址V中存储着变量值A,当前版本是01。线程1获得了当前值A和版本号01,想要更新为B,但是被阻塞了。

这时候,内存地址V中的变量发生了多次改变,版本号提升为03,但是变量值仍是A。

随后线程1恢复运行,进行比较Compare操作,经过比较,线程1所获得的值和地址V中的实际值都是A,但是版本号不相等,所以更新失败。

 

Java中,AtomicStampedReference类就实现了用版本号做比较的CAS机制。

<think>嗯,用户问到了CAS机制,我需要详细解释一下。首先,CAS的全称是Compare-And-Swap,这是一个并发编程中的原子操作。应该先介绍它的基本概念,然后再讲工作原理。记得要分步骤,结构清晰。 CAS机制主要用于解决多线程环境下的数据竞争问题。比如,当多个线程同时修改一个共享变量时,如果不加控制,可能会导致数据不一致。CAS通过比较内存中的值和预期值是否一致来决定是否更新,这样可以避免使用锁,减少线程阻塞。 接下来要解释CAS的操作步骤。通常分为三个步骤:读取内存值、比较内存值和预期值、如果相同则写入新值。这个过程需要是原子性的,否则还是会有问题。所以需要强调原子操作的重要性,可能提到处理器指令的支持,比如x86的CMPXCHG。 然后可以举例说明CAS的应用场景,比如Java中的AtomicInteger类,或者无锁数据结构的实现。同时,需要指出CAS的优缺点。优点是没有锁的开销,提高性能;缺点是ABA问题,以及自旋导致的CPU资源消耗。 ABA问题需要详细说明一下。比如,一个线程读取内存值为A,然后其他线程将其改为B,再改回A。这时候CAS会认为值没变,但实际上已经被修改过。解决办法可以用版本号或时间戳,比如AtomicStampedReference。 可能用户对并发编程有一定了解,但不太清楚具体实现机制。所以需要避免过于技术化的术语,用简单易懂的语言解释。同时,注意用户要求的结构清晰,分点说明,帮助用户逐步理解。 最后,总结CAS的重要性,在哪些场景下适用,以及使用时需要注意的问题。确保回答真实可靠,涵盖关键点,没有遗漏重要信息。</think>### CAS机制详解(分步理解) --- #### 一、基本概念 **CAS**(Compare-And-Swap,比较并交换)是一种**无锁并发控制技术**,用于实现多线程环境下对共享变量的原子操作。其核心思想是: 1. **比较**:在修改数据前,检查内存中的当前值是否等于预期值。 2. **交换**:若相等,则将新值写入内存;否则放弃操作或重试。 --- #### 二、工作原理(三步流程) 1. **读取内存值** 线程从内存中读取共享变量的当前值,记为`V_old`。 2. **计算新值** 基于`V_old`生成新值`V_new`(例如`V_new = V_old + 1`)。 3. **原子操作** 执行CAS指令: - **条件**:若此时内存中的值仍为`V_old` → 将`V_new`写入内存,返回操作成功。 - **否则** → 放弃操作,返回失败(通常需重试)。 $$ \text{CAS}(V_{\text{old}}, V_{\text{new}}) = \begin{cases} \text{成功} & \text{当且仅当当前内存值等于} V_{\text{old}} \\ \text{失败} & \text{否则} \end{cases} $$ --- #### 三、关键技术特点 1. **原子性** CAS操作由**CPU硬件指令直接支持**(如x86的`CMPXCHG`),确保不可中断。 2. **无锁设计** 无需传统锁机制(如synchronized或ReentrantLock),避免了线程阻塞。 3. **自旋重试** 失败后通常通过循环(自旋)重新尝试,直到成功: ```java do { V_old = get(); // 读取当前值 V_new = calculate(V_old); // 计算新值 } while (!CAS(V_old, V_new)); // 循环直到CAS成功 ``` --- #### 四、典型应用场景 1. **原子类** Java中的`AtomicInteger`、`AtomicLong`等类通过CAS实现原子增减: ```java AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 内部使用CAS ``` 2. **无锁数据结构** 如无锁队列(Lock-Free Queue)、无锁栈(Lock-Free Stack)。 3. **乐观锁** 数据库乐观锁或分布式系统中的版本控制(如Redis的WATCH命令)。 --- #### 五、CAS的局限性 1. **ABA问题** - **现象**:线程1读取值A → 线程2将A→B→A → 线程1的CAS误认为值未变。 - **解决方案**:使用带版本号的原子引用(如Java的`AtomicStampedReference`)。 2. **高竞争下的性能问题** 若线程竞争激烈,频繁自旋会导致CPU资源浪费。 3. **单变量限制** CAS只能保证单个变量的原子性,无法直接处理复合操作。 --- #### 六、代码示例(Java) ```java import java.util.concurrent.atomic.AtomicInteger; public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInt = new AtomicInteger(0); // 模拟10个线程并发递增 for (int i = 0; i < 10; i++) { new Thread(() -> { int oldValue; int newValue; do { oldValue = atomicInt.get(); newValue = oldValue + 1; } while (!atomicInt.compareAndSet(oldValue, newValue)); }).start(); } } } ``` --- #### 七、总结 - **适用场景**:低竞争环境、简单原子操作。 - **避坑指南**:注意ABA问题,避免在高竞争场景滥用自旋。 - **替代方案**:高并发场景可考虑`LongAdder`(分段CAS优化)或锁机制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值