前言:
今天在深入了解HashMap时,看到这句话:“concurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized。”
哦~~这个CAS好像之前接触过,又不记得了~遂重新复习总结一下
目录
一、CAS基础知识
1.CAS概念:
2.CAS 所需3个参数:
3.CAS 操作过程:
4.举例子说明CAS
二、CAS性质
1.什么是乐观锁?什么是悲观锁?
2.CAS 实现的原子性
3.CAS 的优点
4.CAS 的缺点
5.CAS的实际应用场景:
三、Java编程模实现CAS操作
1.例子A:
2.例子B(存在自旋现象)
一、CAS基础知识
1.CAS概念:
CAS(Compare-And-Swap)是一种 乐观锁 机制,在并发编程中被广泛应用。它的核心思想是让线程假设在执行某个操作时不会发生冲突,因此尝试直接进行操作。如果没有其他线程同时修改同一个数据,操作就能顺利完成;如果发生冲突,线程会检测到这一点,并再次尝试,直到操作成功为止。
2.CAS 所需3个参数:
- 内存位置(V):要操作的变量的内存地址。
- 期望值(E):执行操作时,线程期望变量的当前值。如果该值与实际值一致,则认为没有其他线程修改过该变量。
- 新值(N):如果期望值与变量当前值相等,则将变量更新为新值。
3.CAS 操作过程:
- 线程读取内存位置
V
的值,并比较它是否等于期望值E
。- 如果
V == E
,则将V
设置为N
,操作成功。- 如果
V != E
,说明在此期间已经有其他线程修改了该变量,操作失败,线程会进行重试。
4.举例子说明CAS
a:现实生活中的例子
- 我跟小明说:空调开太冷啦,你去把空调从26度,调节到28度。
- 小明是一个只会CAS的呆瓜,它接收到的指令是:V:当前空调上显示的温度,E:26,N:28
- 此时一只小猫不小心按到遥控器,把空调设置成了27度!
- 小明刚想调节温度时发现:此时V=27,不等于E的26,呆瓜小明该次操作失败
二、CAS性质
1.什么是乐观锁?什么是悲观锁?
假如这两个锁会说人话:
乐观锁:我超乐观的!我觉得我要修改的资源肯定没有人线程用嘻嘻,那我就直接修改啰~如果提交时发现有人把我的改了,我就再改一遍(重试)。
悲观锁:呜呜呜一定会有其他线程跟我一起要修改这个资源QAQ,我得先把房门锁上(加锁),不让别的线程进来,没有线程可以影响我,等我改完再把门敞开。
乐观锁、悲观锁概念:
乐观锁:假设在大多数情况下,多个线程不会发生冲突,因此允许多个线程并发地读取和修改共享资源。在操作完成时,检查是否有其他线程修改了数据。如果发现冲突,乐观锁会重试操作,直到操作成功为止。CAS操作就是乐观锁的常见实现。
悲观锁:假设在并发环境中冲突会经常发生,因此在访问共享资源时,线程会采取加锁的方式来确保数据的一致性。在一个线程对资源加锁后,其他线程需要等待锁被释放才能访问该资源。
2.CAS 实现的原子性
CAS 的原子性通常由硬件支持的指令集来保障。例如,在 x86 体系结构中,
cmpxchg
指令就是实现 CAS 的底层支持。该指令可以确保比较和交换操作是一个不可分割的原子操作,从而防止多个线程在同一时刻同时对共享变量进行修改。
3.CAS 的优点
无锁化:CAS 不依赖于锁,不会造成线程阻塞。相比传统的加锁机制,避免了线程上下文切换的开销,提高了系统的并发性能。
高效:在大多数情况下,冲突发生的概率较低,因此 CAS 操作可以直接完成修改操作。即使冲突发生,由于只需要重试,操作成本也相对较小。
非阻塞:当一个线程在执行 CAS 操作时,即使另一个线程同时进行相同操作,它们不会互相阻塞,反而会竞争性地更新数据。这种无锁机制适用于高并发场景。
4.CAS 的缺点
ABA 问题:CAS 只能检测到变量的值是否发生变化,但无法感知值的多次变更。例如,变量从
A
变为B
,再从B
变回A
,CAS 认为数据没有变化,因此会认为操作是安全的。这种情况就是 ABA 问题。解决方案:可以通过引入 版本号 来避免 ABA 问题。每次变量修改时,除了更新变量的值,还更新一个版本号。如果版本号发生了变化,即使数据看似没有变,CAS 仍然能检测到。Java 中的
AtomicStampedReference
类就提供了这种机制。自旋开销:如果 CAS 操作失败,线程通常会采用自旋重试的方式来尝试更新。虽然 CAS 不会阻塞线程,但在高并发的环境下,多个线程同时修改同一变量可能导致频繁的 CAS 失败,增加了 CPU 的开销。
只能保证单个变量的原子操作:CAS 只能对一个共享变量进行原子操作,如果需要同时更新多个变量,CAS 无法保证多个变量之间的操作是原子的。这种情况下,需要使用传统的锁机制来解决。
5.CAS的实际应用场景:
高并发计数器:像
AtomicInteger
、AtomicLong
等类都是基于CAS实现的,它们广泛应用于高并发环境中的计数器操作,例如统计请求数、处理任务数等。并发队列:
ConcurrentLinkedQueue
是一个无锁的并发队列,内部通过CAS实现队列的入队和出队操作,保证高效地处理高并发下的元素操作。多线程下的资源状态更新:CAS可以用于实现无锁的状态更新,比如某些多线程应用中常见的标志位更新或任务状态切换。
线程池中的任务处理:在Java线程池的实现中,很多操作是基于CAS进行的,如任务计数、线程状态的管理等,确保在高并发下的高效调度。
三、Java编程模实现CAS操作
1.例子A:
还是用上面小明开空调的例子,我们来写一个代码:
import java.util.concurrent.atomic.AtomicInteger;
public class AirConditionerCAS {
// 使用 AtomicInteger 来模拟共享的温度变量
private static AtomicInteger temperature = new AtomicInteger(26);
public static void main(String[] args) {
// 小猫不小心按到遥控器,把温度设成了 27 度
temperature.set(27);
// 小明尝试将温度从 26 度调整到 28 度
int expectedValue = 26;
int newValue = 28;
// 小明进行 CAS 操作,只有当当前值等于 26 时才会更新为 28
boolean success = temperature.compareAndSet(expectedValue, newValue);
if (success) {
System.out.println("温度成功调整为: " + temperature.get() + " 度");
} else {
System.out.println("调节失败,当前温度是: " + temperature.get() + " 度");
}
}
}
解释代码:
AtomicInteger temperature
:这是一个AtomicInteger
类型,用来模拟共享的整数变量。AtomicInteger
内部使用 CAS 操作来保证线程安全。
compareAndSet(expectedValue, newValue)
:这个方法实现了 CAS 操作。它会检查当前的temperature
是否等于expectedValue
,如果相等,则更新为newValue
;如果不相等,则不做任何修改,并返回false。
运行结果:
如我们所料。
读compareAndSet的
源码我们看到:
该方法是通过调用底层的方法实现的。注意这个例子中是一个最底层的CAS的原子操作,不存在自旋现象。下面举一个会存在自旋现象的。
2.例子B(存在自旋现象)
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
// 使用AtomicInteger来实现线程安全的计数器
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// 创建100个线程,每个线程对counter进行1000次递增操作
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 递增操作
counter.incrementAndGet(); // 实现CAS自旋锁
}
});
threads[i].start();
}
// 等待所有线程执行完
for (Thread t : threads) {
t.join();
}
// 最终counter的值应该是100000
System.out.println("Final counter value: " + counter.get());
}
}
运行结果:
成功实现多线程并发增加变量。
关于counter.incrementAndGet()方法的解释:
本质还是通过while循环,不断尝试调用底层的原子方法。