关于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来实现。

 
可是CAS也会有自己的问题。因为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 问题

 

posted @   诸葛匹夫  阅读(49)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示