关于java中CAS会引发的ABA问题探究
在并发环境下,为了保证并发安全问题,通常我们会进行加锁操作,比如加上synchronized关键字。但是很多情况下,我们不需要这样的重量级锁,比如说多个线程对某个int类型变量i
进行++操作,但是不加锁吧,又怕影响结果,因为i++不是一个原子操作,会出现并发问题,我们来看个案例。
1 public class NonAtomicIncrementDemo { 2 // 共享变量 3 private static int i = 0; 4 5 public static void main(String[] args) throws InterruptedException { 6 // 线程数量 7 int threadCount = 10; 8 // 每个线程执行的操作次数 9 int incrementCount = 1000; 10 11 // 创建线程数组 12 Thread[] threads = new Thread[threadCount]; 13 14 // 创建并启动线程 15 for (int j = 0; j < threadCount; j++) { 16 threads[j] = new Thread(() -> { 17 for (int k = 0; k < incrementCount; k++) { 18 // 非原子操作 i++ 19 i++; 20 } 21 }); 22 threads[j].start(); 23 } 24 25 // 等待所有线程执行完毕 26 for (Thread thread : threads) { 27 thread.join(); 28 } 29 30 // 预期结果 31 int expectedResult = threadCount * incrementCount; 32 // 实际结果 33 int actualResult = i; 34 35 System.out.println("预期结果: " + expectedResult); 36 System.out.println("实际结果: " + actualResult); 37 } 38 }
执行的结果
问题原因:由于 i++ 不是原子操作,多个线程可能会同时读取 i 的值,然后对其进行加 1 操作,最后再写回。这样就可能会导致某些线程的加 1 操作被覆盖,从而使最终结果小于预期值
解决方法:可以使用 Java 提供的原子类(如 AtomicInteger)来保证 i++ 操作的原子性,或者使用 synchronized 关键字来对 i++ 操作进行同步。
以下是使用 AtomicInteger 解决该问题的示例代码:
1 import java.util.concurrent.atomic.AtomicInteger; 2 3 public class AtomicIncrementDemo { 4 // 原子整数 5 private static AtomicInteger atomicInteger = new AtomicInteger(0); 6 7 public static void main(String[] args) throws InterruptedException { 8 // 线程数量 9 int threadCount = 10; 10 // 每个线程执行的操作次数 11 int incrementCount = 1000; 12 13 // 创建线程数组 14 Thread[] threads = new Thread[threadCount]; 15 16 // 创建并启动线程 17 for (int j = 0; j < threadCount; j++) { 18 threads[j] = new Thread(() -> { 19 for (int k = 0; k < incrementCount; k++) { 20 // 原子操作 incrementAndGet() 21 atomicInteger.incrementAndGet(); 22 } 23 }); 24 threads[j].start(); 25 } 26 27 // 等待所有线程执行完毕 28 for (Thread thread : threads) { 29 thread.join(); 30 } 31 32 // 预期结果 33 int expectedResult = threadCount * incrementCount; 34 // 实际结果 35 int actualResult = atomicInteger.get(); 36 37 System.out.println("预期结果: " + expectedResult); 38 System.out.println("实际结果: " + actualResult); 39 } 40 }
在这个示例中,使用 AtomicInteger 的 incrementAndGet() 方法来保证 i++ 操作的原子性,从而避免了线程安全问题。
incrementAndGet底层就是用了CAS操作。
CAS 的基本思路就是, 如果这个地址上的值和期望的值相等, 则给其赋予新 值, 否则不做任何事儿,但是要返回原值是多少。 自然 CAS 操作执行完成时,
在 业务上不一定完成了, 这个时候我们就会对 CAS 操作进行反复重试, 于是就有了 循环 CAS。很明显, 循环 CAS 就是在一个循环里不断的做 cas 操作, 直到成功为 止。 Java 中的 Atomic 系列的原子操作类的实现则是利用了循环 CAS来实现。
一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行 检查时会发现它的值没有发生变化,
但是实际上却变化了。
1 public class ABAProblemExample { 2 // 创建一个原子整数,初始值为 A 3 private static AtomicInteger atomicInteger = new AtomicInteger(100); 4 5 public static void main(String[] args) { 6 // 线程 1 模拟正常的操作 7 Thread thread1 = new Thread(() -> { 8 int expectedValue = atomicInteger.get(); 9 System.out.println("线程 1 获取到的初始值: " + expectedValue); 10 11 // 模拟一些耗时操作 12 try { 13 Thread.sleep(2000); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 18 // 尝试使用 CAS 操作更新值 19 boolean result = atomicInteger.compareAndSet(expectedValue, 200); 20 if (result) { 21 System.out.println("线程 1 更新值成功,新值为: " + atomicInteger.get()); 22 } else { 23 System.out.println("线程 1 更新值失败"); 24 } 25 }); 26 27 // 线程 2 模拟 ABA 问题 28 Thread thread2 = new Thread(() -> { 29 // 模拟一些延迟,确保线程 1 先获取到初始值 30 try { 31 Thread.sleep(500); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } 35 36 // 将值从 A 变为 B 37 atomicInteger.compareAndSet(100, 150); 38 System.out.println("线程 2 将值从 100 变为 150,当前值: " + atomicInteger.get()); 39 40 // 模拟一些操作 41 try { 42 Thread.sleep(500); 43 } catch (InterruptedException e) { 44 e.printStackTrace(); 45 } 46 47 // 将值从 B 变回 A 48 atomicInteger.compareAndSet(150, 100); 49 System.out.println("线程 2 将值从 150 变回 100,当前值: " + atomicInteger.get()); 50 }); 51 52 thread1.start(); 53 thread2.start(); 54 55 try { 56 thread1.join(); 57 thread2.join(); 58 } catch (InterruptedException e) { 59 e.printStackTrace(); 60 } 61 } 62 }
执行的结果:
从执行的结果来看,大家感觉没问题啊,我线程1就是要将值变为200,结果是对的啊。看上去最终的结果是对的,那ABA问题也不是什么大问题啊。
我们思考一个问题:
在一个账户系统中,有一个原子整数表示账户余额。
用户 A 要从账户中取出 100 元,
此时系统先记录当前余额(假设为 500 元)准备进行 CAS 操作。
与此同时,用户 B 向该账户存入 100 元,之后又取出 100 元,
使得账户余额又变回 500 元。这时用户 A 的 CAS 操作会认为余额没有变化而成功执行取款操作,
但实际上账户的交易历史已经改变,可能会导致后续的账务统计、审计等功能出现错误
解决 ABA 问题的方法:
可以使用 AtomicStampedReference 类,它在进行 CAS 操作时不仅比较值,
还比较一个版本号(时间戳),从而避免 ABA 问题。
以下是使用 AtomicStampedReference 解决 ABA 问题的示例代码:
1 import java.util.concurrent.atomic.AtomicStampedReference; 2 3 public class SolveABAProblemExample { 4 private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 0); 5 6 public static void main(String[] args) { 7 // 线程 1 模拟正常的操作 8 Thread thread1 = new Thread(() -> { 9 int[] stampHolder = new int[1]; 10 int expectedValue = atomicStampedReference.get(stampHolder); 11 int expectedStamp = stampHolder[0]; 12 System.out.println("线程 1 获取到的初始值: " + expectedValue + ",版本号: " + expectedStamp); 13 14 // 模拟一些耗时操作 15 try { 16 Thread.sleep(2000); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 21 // 尝试使用 CAS 操作更新值 22 boolean result = atomicStampedReference.compareAndSet(expectedValue, 200, expectedStamp, expectedStamp + 1); 23 if (result) { 24 System.out.println("线程 1 更新值成功,新值为: " + atomicStampedReference.getReference() + ",新版本号: " + atomicStampedReference.getStamp()); 25 } else { 26 System.out.println("线程 1 更新值失败"); 27 } 28 }); 29 30 // 线程 2 模拟 ABA 问题 31 Thread thread2 = new Thread(() -> { 32 // 模拟一些延迟,确保线程 1 先获取到初始值 33 try { 34 Thread.sleep(500); 35 } catch (InterruptedException e) { 36 e.printStackTrace(); 37 } 38 39 int[] stampHolder = new int[1]; 40 int value = atomicStampedReference.get(stampHolder); 41 int stamp = stampHolder[0]; 42 43 // 将值从 A 变为 B 44 atomicStampedReference.compareAndSet(value, 150, stamp, stamp + 1); 45 System.out.println("线程 2 将值从 100 变为 150,当前值: " + atomicStampedReference.getReference() + ",版本号: " + atomicStampedReference.getStamp()); 46 47 // 模拟一些操作 48 try { 49 Thread.sleep(500); 50 } catch (InterruptedException e) { 51 e.printStackTrace(); 52 } 53 54 value = atomicStampedReference.get(stampHolder); 55 stamp = stampHolder[0]; 56 57 // 将值从 B 变回 A 58 atomicStampedReference.compareAndSet(value, 100, stamp, stamp + 1); 59 System.out.println("线程 2 将值从 150 变回 100,当前值: " + atomicStampedReference.getReference() + ",版本号: " + atomicStampedReference.getStamp()); 60 }); 61 62 thread1.start(); 63 thread2.start(); 64 65 try { 66 thread1.join(); 67 thread2.join(); 68 } catch (InterruptedException e) { 69 e.printStackTrace(); 70 } 71 } 72 }
在这个示例中,AtomicStampedReference 会同时比较值和版本号,当值和版本号都匹配时才会执行更新操作,从而避免了 ABA 问题
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?