目录
1、CAS的原理
CAS 即 compare and swap(比较与交换)
,是一种乐观锁机制,也叫无锁机制。即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)
CAS 中涉及三个要素:
-
需要读写的内存值 V
-
进行比较的值 A
-
拟写入的新值 B
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
CAS的原子性
CAS操作是原子的,这意味着它要么完全执行成功,要么完全不执行。即使在多线程环境中,也不会有其他线程能够干扰CAS操作的执行
在之前文章Java并发—volatile关键字的作用及使用场景-CSDN博客中使用了volatile关键字修改变量x的过程,但修改和写回操作不是原子性,还是会有数据不一致性的问题,CAS就可以解决这个问题,那使用CAS操作来修改共享变量x,流程又会是如何呢?
还是使用之前的案例:🌰线程A和线程B从主内存读取和修改x=1的过程
步骤:
1.初始化:x=1,存储在主内存
2.线程A读取:A从主内存读取,复制x=1到A的工作内存
3.线程B读取:B从主内存读取x,复制x=1到B的工作内存
4.线程A修改:A在工作内存中修改x=2
5.线程A尝试CAS写回:A将工作内存中的x=2与主内存中的x=1进行比较
如果主内存中仍然是x=1(预期值),则CAS操作成功,x=1更新至x=2
如果主内存中x≠1(被其他线程修改),则CAS操作失败,线程A需要重新读取更新的值并再次尝试重新读取最新的值
6.线程B修改:B在工作内存中修改x=3
7.线程B尝试CAS写回
如果主内存中x=1(这是B读取时的值,但此时可能已经被A修改x=2),也就是线程B以为主内存中是x=1(预期值),但实际上主内存是x=2(实际值),预期值与实际值不符,则 CAS操作失败
B需要重新读取最新的值x=2,并尝试CAS操作
8.线程B重新读取:x=2
9.线程B再次尝试CAS写回:B再次尝试将x=2更新至x=3,如果此时没有其他线程修改x,则CAS操作成功
由上述流程可以看出当使用CAS操作来修改共享变量x时,与volatile修改共享变量x流程会有所不同,CAS也成功得解决了上一篇文章中遗留问题
2、CAS的问题
看起来好像又没问题了,但CAS会出现ABA问题
1)ABA问题
❓什么是ABA问题呢?
ABA 问题说的是,如果一个变量第一次读取的值是 A,准备好需要对 A 进行写操作的时候,发现值还是 A,那么这种情况下,能认为 A 的值没有被改变过吗?可以是由 A -> B -> A 的这种情况
❓那如何解决呢?
为了解决ABA问题,常见的做法是在CAS操作中加入版本号或标记。例如,可以使用 AtomicStampedReference 类,它不仅保存了值,还保存了一个版本号或标记。每次值发生变化时,版本号都会递增。这样,在进行CAS操作时,compareAndSet()不仅要比较值是否相同,还要比较版本号是否相同,从而避免了ABA问题。
这样,即使值回到了原来的状态,只要版本号不同,CAS操作就会失败,从而确保了操作的正确性
AtomicInteger 使用 CAS 操作来实现线程安全的整数操作。
来看一下AtomicInteger的源码,AtomicInteger 的核心在于使用了 Unsafe 类的 native() 来执行 CAS 操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
-
UnSafe 是一个 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地 native() 进行访问 UnSafe 相当于一个后门,基于该类可直接操作特定的内存数据
-
value 用 volatile 修饰,保证了多线程之间内存可见性
-
valueOffset它表示 AtomicInteger 类中 value 字段相对于对象起始地址的偏移量,valueOffset 计算了 value 字段相对于对象起始地址的位置
AtomicInteger 提供了 compareAndSet 方法来执行 CAS 操作。这个方法接收期望的旧值和新值,如果当前值等于期望的旧值,则原子地更新为新值,并返回 true,否则返回 false
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe.compareAndSwapInt 方法是一个 native() 方法,它允许在 Java 中直接使用底层硬件提供的 CAS 指令来实现原子操作,工作流程如下:
- 读取当前值:首先读取对象中指定偏移量位置的值
- 比较值:将读取到的值与 expectedValue 进行比较
- 条件更新:如果当前值与 expectedValue 相等,则使用 CAS 操作将值更新为 newValue;否则,不进行任何操作
- 返回结果:返回一个布尔值,表示 CAS 操作是否成功。如果成功,则返回 true;否则返回 false
如果 CAS 操作失败(即当前值与期望值不匹配),则表明有其他线程已经修改了该值,此时调用方可以选择重试操作或采取其他措施
例如:
AtomicInteger value = new AtomicInteger(0);
// 尝试将value从0更新为5,如果当前值是0
boolean result = value.compareAndSet(0, 5);
System.out.println("Update successful: " + result);
System.out.println("New value: " + value.get());
上面是AtomicInteger展示了原子操作,但实际上并不能解决ABA问题
一般大家比较熟悉的是AtomicInteger, AtomicLong, AtomicReference ,那与AtomicStampedReference 失败有什么不同呢?
AtomicStampedReference 和 AtomicInteger 都是 Java 并发工具包(java.util.concurrent.atomic 包)中提供的原子类,用于在多线程环境中提供原子操作
在包中,只用AtomicStampedReference ,AtomicStampedReference 是一个更通用的原子引用类,它不仅提供了原子的引用更新,还提供了一个“戳记”(stamp)或版本号,用于解决原子更新中的ABA问题
AtomicStampedReference 保存了一个对象引用和一个与之关联的整型戳记,每次更新引用时,戳记也会被更新
这样,即使引用回到了原始值,通过检查戳记也可以知道引用实际上曾经被修改过
🌰:假设线程A和线程B,都在尝试购买同一活动的门票
可能会出现:
1、线程A读取门票状态:剩余100张。
2、线程B读取门票状态:同样看到剩余100张。
3、线程A尝试购买:线程A尝试购买10张门票,将剩余数量更新为90张。
4、线程B尝试购买:在不知道线程A已经购买的情况下,线程B也尝试购买10张门票,但由于没有版本号,线程B认为剩余数量仍然是100张
问题:
ABA问题:尽管线程B读取的门票状态和尝试更新后的状态与线程A的相同(即从100到90),但由于线程B没有检查版本号,它会错误地覆盖线程A的更新,导致实际上只减少了10张门票,而不是20张
后果:
业务逻辑错误:系统可能会错误地允许更多的用户购买超出实际剩余数量的门票,导致超售或欠售问题。
用户体验差:用户可能会遇到不可预测的错误或不公平的购票机会
如何使用AtomicStampedReference解决呢?
public class TicketSalesSystem {
// 创建 AtomicStampedReference 对象,用于原子操作更新门票引用和版本号
private final AtomicStampedReference<Ticket> ticketRef = new AtomicStampedReference<>(new Ticket("EVT123", 100), 0);
// 函数用于尝试购买门票
public boolean buyTicket(String eventId, int ticketsToBuy) {
Ticket currentTicket;
int stamp;
do {
// 读取当前的门票和版本号
currentTicket = ticketRef.getReference();
stamp = ticketRef.getStamp();
// 检查门票是否属于正确的活动
if (!currentTicket.getEventId().equals(eventId)) {
return false;
}
// 检查是否有足够的剩余门票
if (currentTicket.getRemainingTickets() < ticketsToBuy) {
return false;
}
// 减少剩余门票数量
currentTicket.setRemainingTickets(currentTicket.getRemainingTickets() - ticketsToBuy);
} while (!ticketRef.compareAndSet(currentTicket, currentTicket, stamp, stamp + 1));
System.out.println("您成功购买 " + eventId + "活动" ticketsToBuy + "张票");
return true;
}
}
模拟抢票
TicketSalesSystem system = new TicketSalesSystem();
// 线程A尝试购买门票
Thread threadA = new Thread(() -> system.buyTicket("EVT123", 10));
// 线程B尝试购买门票
Thread threadB = new Thread(() -> system.buyTicket("EVT123", 10));
// 启动线程
threadA.start();
threadB.start();
try {
// 等待线程结束
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的门票状态
System.out.println("最终买票状态: " + system.ticketRef.getReference());
在这个例子中,TicketSalesSystem 类使用 AtomicStampedReference 来原子地更新门票的状态。buyTicket 方法尝试购买一定数量的门票,如果当前门票的剩余数量足够,它会减少剩余门票的数量。compareAndSet 方法用于确保更新操作是原子的,并且解决了ABA问题。
如果在尝试更新门票时遇到ABA问题,compareAndSet 方法会返回 false,这时 buyTicket 方法会重新读取当前的门票和版本号,然后再次尝试更新,直到成功为止
但值得注意的是:这里只是修改ticketsToBuy一个变量,由上可知CAS只能保证单一变量的原子性,那多个变量呢?就需要多个CAS操作和其他技术
案例:假设线程A和线程B,都在尝试购买不同活动的门票
TicketInfo 类来封装购票数量和活动 ID,使用 AtomicReference 类,以原子地更新一个对象引用
public class TicketWithCAS {
private AtomicInteger remainingTickets = new AtomicInteger(100); // 剩余票数
private AtomicReference<TicketInfo> ticketInfo = new AtomicReference<>(new TicketInfo(0, 0)); // 购票信息
public void buyTicket(int ticketsToBuy, int eventId) {
while (true) {
int currentRemaining = remainingTickets.get();
if (currentRemaining >= ticketsToBuy) {
TicketInfo currentInfo = ticketInfo.get();
if (ticketInfo.compareAndSet(currentInfo, new TicketInfo(ticketsToBuy, eventId))) {
if (remainingTickets.compareAndSet(currentRemaining, currentRemaining - ticketsToBuy)) {
System.out.println(Thread.currentThread().getName() + " 成功购买 " + ticketsToBuy + " 张票");
System.out.println("剩余票数: " + remainingTickets.get());
break;
} else {
// 如果剩余票数发生变化,需要重试
continue;
}
}
} else {
System.out.println(Thread.currentThread().getName() + " 购票失败,剩余票数不足");
break;
}
}
}
}
模拟购票:
// 创建 TicketWithCAS 对象
TicketWithCAS ticketWithCAS = new TicketWithCAS();
// 创建线程模拟购票
Thread thread1 = new Thread(() -> { ticketWithCAS.buyTicket(10, 1);}, "Thread 1");
Thread thread2 = new Thread(() -> { ticketWithCAS.buyTicket(20, 2);}, "Thread 2");
Thread thread3 = new Thread(() -> { ticketWithCAS.buyTicket(30, 3);}, "Thread 3");
// 启动线程
thread1.start();
thread2.start();
thread3.start();
使用 CAS 来保证多个变量的一致性较为复杂,因为 CAS 操作只能保证单个变量的原子性。为了实现多变量的一致性,我们可以使用 AtomicReference 类,它可以用来原子地更新一个对象引用
因此,为了避免这些问题,我们需要引入版本号或类似机制(例如:synchronized关键字和ReentrantLock)
实际上,AtomicStampedReference 实际上是一种版本号机制
说到版本号机制,在实际工作中,通常选择使用数据库自带的 version 字段作为版本号
2)自旋锁消耗问题
使用 CAS 操作时,当 CAS 操作失败时,线程通常会进入自旋状态,不断尝试重新执行 CAS 操作(通常是一个 do-while 循环),直到成功为止。这种自旋重试机制在某些情况下可能会导致 CPU 资源的过度消耗,尤其是在高并发场景下
如果某个线程一直取到的值和预期值都不一样,这样就会无限循环
解决:在自旋次数达到一定阈值后,切换到阻塞模式,使用传统的锁机制
3)多变量共享一致性问题
CAS只能保证一个共享变量的原子操作
当需要对多个变量进行原子操作时,CAS 只能保证单个变量的原子性,而无法保证多个变量之间的原子性。这可能会导致数据的一致性问题
解决:
①对于两个或更多变量,可以使用复合 CAS 操作来实现。这通常涉及到将多个变量组合成一个复合对象,并使用 AtomicReference 来更新这个复合对象
②使用锁或其他并发控制机制来保证多个变量的一致性,如 ReentrantLock 或 synchronized 关键字
4)不可中断性
CAS 操作通常是不可中断的,这意味着在进行 CAS 操作时,如果发生中断,可能会导致线程挂起或行为异常。在某些情况下,这可能会导致死锁或资源泄漏等问题
解决:在可能的情况下,使用可中断的锁或其他机制,以便在必要时能够中断线程,如ReentrantLock
3、CAS的应用
①无锁数据结构:实现线程安全的无锁队列、栈、哈希表等数据结构,如ConcurrentLinkedQueue, ConcurrentSkipListMap,它们利用 CAS 来保证在高并发下的性能和一致性
①并发集合:如 ConcurrentHashMap 使用 CAS 结合锁分段技术来实现高效的并发读写操作
③无锁计数器:使用CAS实现一个高效的无锁计数器,避免了使用同步锁带来的性能开销
4、总结
CAS通过比较并交换的方式来更新共享变量,避免了使用传统锁机制带来的性能开销,但在高并发场景下可能会遇到自旋消耗CPU资源和ABA问题等