CAS中的ABA问题以及解决方案
ABA问题
在没有加版本号之前,CAS会出现ABA问题:当一个值原本已经被当前线程读取到,准备通过CAS(自旋锁)将其修改的时候,突然这个时候由于网络卡顿、线程中断等一系列状况的原因,中途来了另外一个线程,将当前线程所期望的值修改成其他的值,然后又修改回来,这期间当前线程没有察觉,看了下此时的值跟预取的值相同,就直接修改了。坦白说,这种情况如果不是在很严格的场景下,其实是无关紧要的,但是如果是涉及到银行系统的资金流转问题之类的就是相当严重了。所以说,ABA问题我们还是有必要解决的,而且要很好的解决。
存在ABA问题的代码
1 package com.lzp.juc; 2 3 import java.util.concurrent.atomic.AtomicInteger; 4 5 /** 6 * @Author LZP 7 * @Date 2021/6/10 13:24 8 * @Version 1.0 9 */ 10 public class ABA { 11 12 static volatile AtomicInteger a = new AtomicInteger(); 13 14 public static void main(String[] args) { 15 // 模拟两个线程 16 /* 17 第一个线程去获取a的值,然后休眠两秒(即模拟时间片用完了,为的是让另一个线程去重现ABA现象) 18 一开始第二个线程需要休眠20ms,其目的是让第一个线程先将其a的值读到自己的栈空间,然后自己再 19 去操作a的值 20 */ 21 Thread main = new Thread(() -> { 22 int exceptVal = a.get(); 23 System.out.println(Thread.currentThread().getName() + "第一次获取到的值:" + exceptVal); 24 try { 25 // 先休眠1s 26 Thread.sleep(1000); 27 } catch (InterruptedException e) { 28 e.printStackTrace(); 29 } 30 // 然后再去获取a的值 31 System.out.println(Thread.currentThread().getName() + "是否修改成功:" + a.compareAndSet(exceptVal, exceptVal + 1)); 32 }, "主线程"); 33 34 Thread ABA = new Thread(() -> { 35 // 先休眠2ms 36 try { 37 Thread.sleep(2); 38 } catch (InterruptedException e) { 39 e.printStackTrace(); 40 } 41 42 // 自增 43 a.incrementAndGet(); 44 System.out.println(Thread.currentThread().getName() + "increment之后的值:" + a.get()); 45 // 自减 46 a.decrementAndGet(); 47 System.out.println(Thread.currentThread().getName() + "decrement之后的值:" + a.get()); 48 }, "制造ABA问题的线程"); 49 50 // 启动线程 51 main.start(); 52 ABA.start(); 53 } 54 }
运行结果:
利用Java中AtomicStampedReference类解决ABA问题
1 package com.lzp.juc; 2 3 import java.util.concurrent.atomic.AtomicStampedReference; 4 5 /** 6 * @Author LZP 7 * @Date 2021/6/10 13:20 8 * @Version 1.0 9 * 10 * CAS中的ABA问题解决 11 * 这里我们不能只用值来判断了,还需要有一个版本号来标识,此时我们就可以 12 * 用jdk中提供的一个类AtomicStampedReference 13 * 14 * 其中有一个私有的静态内部类 15 * private static class Pair<T> { 16 * final T reference; 17 * final int stamp; 18 * private Pair(T reference, int stamp) { 19 * this.reference = reference; 20 * this.stamp = stamp; 21 * } 22 * static <T> Pair<T> of(T reference, int stamp) { 23 * return new Pair<T>(reference, stamp); 24 * } 25 * } 26 * 27 * AtomicStampedReference与AtomicInteger进行对比 28 * 1、AtomicStampedReference类: 29 * 主线程第一次获取到的值:1 30 * 制造ABA问题的线程increment之后的值:2 31 * 制造ABA问题的线程decrement之后的值:1 32 * 主线程是否修改成功:false 33 * 34 * 2、AtomicInteger类: 35 * 主线程第一次获取到的值:0 36 * 制造ABA问题的线程increment之后的值:1 37 * 制造ABA问题的线程decrement之后的值:0 38 * 主线程是否修改成功:true 39 */ 40 public class CASABA { 41 42 static volatile AtomicStampedReference<Integer> a = new AtomicStampedReference<>(1, 1); 43 44 public static void main(String[] args) { 45 // 模拟两个线程 46 /* 47 第一个线程去获取a的值,然后休眠1秒(即模拟时间片用完了,为的是让另一个线程去重现ABA现象) 48 一开始第二个线程需要休眠2ms,其目的是让第一个线程先将其a的值读到自己的栈空间,然后自己再 49 去操作a的值 50 */ 51 Thread main = new Thread(() -> { 52 int exceptRef = a.getReference(); 53 int exceptStamp = a.getStamp(); 54 System.out.println(Thread.currentThread().getName() + "第一次获取到的值:" + exceptRef); 55 try { 56 // 先休眠1s 57 Thread.sleep(1000); 58 } catch (InterruptedException e) { 59 e.printStackTrace(); 60 } 61 // 然后再去获取a的值 62 System.out.println(Thread.currentThread().getName() + "是否修改成功:" + a.compareAndSet(exceptRef, exceptRef + 1, exceptStamp, exceptStamp + 1)); 63 System.out.println("版本号Stamp:" + a.getStamp()); 64 }, "主线程"); 65 66 Thread ABA = new Thread(() -> { 67 // 先休眠2ms 68 try { 69 Thread.sleep(2); 70 } catch (InterruptedException e) { 71 e.printStackTrace(); 72 } 73 74 // 自增 75 a.compareAndSet(a.getReference(), a.getReference() + 1, a.getStamp(), a.getStamp() + 1); 76 System.out.println(Thread.currentThread().getName() + "increment之后的值:" + a.getReference()); 77 // 自减 78 a.compareAndSet(a.getReference(), a.getReference() - 1, a.getStamp(), a.getStamp() + 1); 79 System.out.println(Thread.currentThread().getName() + "decrement之后的值:" + a.getReference()); 80 }, "制造ABA问题的线程"); 81 82 // 启动线程 83 main.start(); 84 ABA.start(); 85 } 86 87 }
运行结果: