并发编程学习笔记(三十、ABA问题)
目录:
- CAS操作步骤
- CAS极端情况下可能会导致的问题:ABA
- ABA问题的影响
- JUC是如何解决ABA问题的
CAS操作步骤
CAS:compare and swap,即比较后再交换。它是一种无锁的算法,操作如下:
- 从内存获取V的值。
- 拿到旧的预期值A。
- 拿到要修改的值B。
- 当且仅当预期值A和内存值V相同的时候,将内存值V修改为B;否则啥都不做。
CAS极端情况下可能会导致的问题:ABA
因为CAS并不是原子操作,所以在并发情况下可能会导致如下问题(当然锁和循环CAS的方式实现原子操作):
- 1、线程1获取到内存值V。
- 2、线程2获取到内存值V。
- 3、线程1将内存值V改成了B。
- 4、线程2还有一些逻辑没做,还未执行到CAS操作(线程B休息了一会)。
- 5、线程1又将值A写入了V。
- 6、线程2执行CAS,发现内存值V与预期值A一致,所以执行了CAS操作,CAS成功。
此时尽管线程2的CAS操作成功了,单着并不代表这个过程是没有问题的,因为对于线程2而言,线程1执行的修改已经丢失了。
所以ABA问题只是CAS使用过程中的极端情况,某个线程将值先改成B又改成A,这样另一个线程CAS的时候会发现旧的值还是A,导致数据不对,这就是ABA问题。
ABA问题的影响
假如我们现在有一个数据结构底层基于栈实现,按照上面那种情况,就会出现如下的问题:
首先我们有一个栈,里面有A、B两个元素,A为栈顶(图1)。
执行到第4步,线程2休眠了一会,此时线程2已经知道A.next = B,将要执行CAS将栈顶替换为B。
执行第5步,线程1将A、B两个元素一次出栈,并又将D、C、A依次入栈,此时对象B处于游离状态,然后栈中元素见图2、图3。
最后当线程2执行CAS操作后,发现栈顶还是A,所以CAS成功,将栈顶替换为B;但实际上B.next = null,所以此时栈中只有B一个元素了,C和D就平白无故的丢了。
JUC是如何解决ABA问题的
上节的AtomicInteger曾说道有两个Atomic类可以避免ABA问题:
- AtomicMarkableReference:内部通过Pari承载引用对象及是否被更新过的标记,避免了BAB问题。
- AtomicStampedeReference:内部通过Pari承载引用对象及更新的邮戳,避免了BAB问题。
AtomicMarkableReference:
AtomicMarkableReference:既然出现ABA问题的原因是无法知道执行CAS操作的线程值有没有更新过,那我干脆就弄一个标识,让这些线程共享这个标识的值不就可以知道原值有没有被更新过了嘛;而AtomicMarkableReference的实现正是这样的。
首先我们来看下AtomicMarkableReference是如何使用的:
1 public class AtomicMarkableReferenceDemo { 2 3 /** 4 * 初始值 5 */ 6 private static final Integer INIT_NUM = 10; 7 8 /** 9 * 临时值 10 */ 11 private static final Integer TEMP_NUM = 20; 12 13 /** 14 * 更新值 15 */ 16 private static final Integer UPDATE_NUM = 100; 17 18 /** 19 * 更新标识 20 */ 21 private static final Boolean INITIAL_MARK = Boolean.FALSE; 22 23 private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(INIT_NUM, INITIAL_MARK); 24 25 public static void main(String[] args) { 26 // 线程2,执行CAS操作的线程 27 new Thread(() -> { 28 System.out.println(Thread.currentThread().getName() + " : 初始值为:" + INIT_NUM + " , 标记为: " + INITIAL_MARK); 29 boolean mark = atomicMarkableReference.isMarked(); 30 try { 31 Thread.sleep(1000); 32 } 33 catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 // 执行CAS 10 ---> 100 37 boolean result = atomicMarkableReference.compareAndSet(INIT_NUM, UPDATE_NUM, mark, Boolean.TRUE); 38 System.out.println("AtomicMarkableReference发生ABA后的执行结果=" + result); 39 }, "线程A").start(); 40 41 // 线程1,修改内存值的线程 42 new Thread(() -> { 43 Thread.yield(); 44 // 初始态 45 System.out.println(Thread.currentThread().getName() + " : 初始值为:" + atomicMarkableReference.getReference() + " , 标记为: " + INITIAL_MARK); 46 // CAS修改 10 ---> 20 47 atomicMarkableReference.compareAndSet(atomicMarkableReference.getReference(), TEMP_NUM, atomicMarkableReference.isMarked(), Boolean.TRUE); 48 System.out.println(Thread.currentThread().getName() + " : 修改后的值为:" + atomicMarkableReference.getReference() + " , 标记为: " + atomicMarkableReference.isMarked()); 49 // CAS修改 20 ---> 10 50 atomicMarkableReference.compareAndSet(atomicMarkableReference.getReference(), INIT_NUM, atomicMarkableReference.isMarked(), Boolean.TRUE); 51 System.out.println(Thread.currentThread().getName() + " : 修改后的值为:" + atomicMarkableReference.getReference() + " , 标记为: " + atomicMarkableReference.isMarked()); 52 53 }, "线程B").start(); 54 } 55 56 }
最终会修改失败,结果如下:
线程A : 初始值为:10 , 标记为: false 线程B : 初始值为:10 , 标记为: false 线程B : 修改后的值为:20 , 标记为: true 线程B : 修改后的值为:10 , 标记为: true AtomicMarkableReference发生ABA后的执行结果=false
原理很简单,你可以自行翻阅:就是通过Unsafe的compareAndSwapObject实现CAS,通过Pair的mark判断有没有改变值。
1 public boolean compareAndSet(V expectedReference, 2 V newReference, 3 boolean expectedMark, 4 boolean newMark) { 5 Pair<V> current = pair; 6 return 7 expectedReference == current.reference && 8 expectedMark == current.mark && 9 ((newReference == current.reference && 10 newMark == current.mark) || 11 casPair(current, Pair.of(newReference, newMark))); 12 }
1 private static class Pair<T> { 2 final T reference; 3 final boolean mark; 4 private Pair(T reference, boolean mark) { 5 this.reference = reference; 6 this.mark = mark; 7 } 8 static <T> Pair<T> of(T reference, boolean mark) { 9 return new Pair<T>(reference, mark); 10 } 11 }
AtomicStampedeReference:
AtomicStampedeReference的原理和makrable的原理很相似,但标记值就并非为boolean了,而是int,类似于mysql中版本号的概念,判断前后的版本号是否一致,一致则更新,不一致则失败。
1 public class AtomicStampedReferenceDemo { 2 3 /** 4 * AtomicInteger计数器 5 */ 6 private static AtomicInteger atomicCounter = new AtomicInteger(100); 7 8 /** 9 * AtomicStampedReference计数器 10 */ 11 private static AtomicStampedReference<Integer> atomicStampedCounter = new AtomicStampedReference<>(100, 0); 12 13 /** 14 * 测试代码 15 */ 16 public static void main(String[] args) throws InterruptedException { 17 // 测试AtomicInteger不会发现ABA问题 18 Thread thread1 = new Thread(() -> { 19 // 100变101 20 atomicCounter.compareAndSet(100, 101); 21 // 101变100 22 atomicCounter.compareAndSet(101, 100); 23 }); 24 // 测试线程2不会发现ABA问题 25 Thread thread2 = new Thread(() -> { 26 try { 27 TimeUnit.SECONDS.sleep(1); 28 } 29 catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 boolean atomicResult = atomicCounter.compareAndSet(100, 101); 33 System.out.println("发生ABA问题时,AtomicInteger执行结果= " + atomicResult); 34 }); 35 36 thread1.start(); 37 thread2.start(); 38 thread1.join(); 39 thread2.join(); 40 41 // 测试AtomicStampedReference会发现ABA问题 42 Thread stamped1 = new Thread(() -> { 43 try { 44 TimeUnit.SECONDS.sleep(1); 45 } 46 catch (InterruptedException e) { 47 e.printStackTrace(); 48 } 49 atomicStampedCounter.compareAndSet(100, 101, atomicStampedCounter.getStamp(), atomicStampedCounter.getStamp() + 1); 50 atomicStampedCounter.compareAndSet(101, 100, atomicStampedCounter.getStamp(), atomicStampedCounter.getStamp() + 1); 51 System.out.println("线程stamped1获取的版本号 = " + atomicStampedCounter.getStamp()); 52 }); 53 Thread stamped2 = new Thread(() -> { 54 // stamp发生变化 55 int stamp = atomicStampedCounter.getStamp(); 56 System.out.println("线程stamped2在ABA发生前获取的版本号 = " + stamp); 57 try { 58 TimeUnit.SECONDS.sleep(2); 59 } 60 catch (InterruptedException e) { 61 e.printStackTrace(); 62 } 63 boolean atomicStampedResult = atomicStampedCounter.compareAndSet(100, 101, stamp, stamp + 1); 64 System.out.println("发生ABA问题时,AtomicStampedReference执行结果= " + atomicStampedResult); 65 }); 66 67 stamped1.start(); 68 stamped2.start(); 69 } 70 71 }
原理类似:
1 public boolean compareAndSet(V expectedReference, 2 V newReference, 3 int expectedStamp, 4 int newStamp) { 5 Pair<V> current = pair; 6 return 7 expectedReference == current.reference && 8 expectedStamp == current.stamp && 9 ((newReference == current.reference && 10 newStamp == current.stamp) || 11 casPair(current, Pair.of(newReference, newStamp))); 12 }
1 private static class Pair<T> { 2 final T reference; 3 final int stamp; 4 private Pair(T reference, int stamp) { 5 this.reference = reference; 6 this.stamp = stamp; 7 } 8 static <T> Pair<T> of(T reference, int stamp) { 9 return new Pair<T>(reference, stamp); 10 } 11 }
——————————————————————————————————————————————————————————————————————