传说中的并发编程ABA问题

什么是ABA问题

ABA并不是一个缩写,更像是一个形象的描述。ABA问题出现在多线程或多进程计算环境中。

首先描述ABA。假设两个线程T1和T2访问同一个变量V,当T1访问变量V时,读取到V的值为A;此时线程T1被抢占了,T2开始执行,T2先将变量V的值从A变成了B,然后又将变量V从B变回了A;此时T1又抢占了主动权,继续执行,它发现变量V的值还是A,以为没有发生变化,所以就继续执行了。这个过程中,变量V从A变为B,再由B变为A就被形象地称为ABA问题了。

上面的描述看上去并不会导致什么问题。T1中的判断V的值是A就不应该有问题的,无论是开始的A,还是ABA后面的A,判断的结果应该是一样的才对。

不容易看出问题的主要还是因为:“值是一样的”等同于“没有发生变化”(就算被改回去了,那也是变化)的认知。毕竟在大多数程序代码中,我们只需要知道值是不是一样的,并不关心它在之前的过程中有没有发生变化;所以,当我需要知道之前的过程中“有没有发生变化”的时候,ABA就是问题了。

现实ABA问题

警匪剧看多了人应该可以快速反应到发生了什么。应用到ABA问题,首先,这里的A和B并不表示被掉的包这个实物,而是掉包过程中的状态的变化。假设一个装有10000W箱子(别管它有多大)放在一个房间里,10分钟后再进去拿出来赎人去。但是,有个贼在这10分钟内进去(别管他是怎么进去的)用一个同样大小的空箱子,把我的箱子掉包了。当我再进去看的时候,发现箱子还在,自然也就以为没有问题了的,就继续拿着桌子上的箱子去赎人了(别管重量对不对)。现在只要知道这里有问题就行了,拿着没钱的箱子去赎人还没有问题么?

这里的变量V就是桌子上是否有箱子的状态。A,是桌子上有箱子的状态;B是箱子在掉包过程中,离开桌子,桌子上没有箱子的状态;最后一个A也是桌子上有箱子的状态。但是箱子里面的东西是什么就不不知道了。

程序世界的ABA问题

在运用CAS做Lock-Free操作中有一个经典的ABA问题:

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,例如下面的例子:

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

  1. import java.util.concurrent.TimeUnit;
  2. import java.util.concurrent.atomic.AtomicInteger;
  3. import java.util.concurrent.atomic.AtomicStampedReference;
  4. public class ABA {
  5. private static AtomicInteger atomicInt = new AtomicInteger(100);
  6. private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);
  7. public static void main(String[] args) throws InterruptedException {
  8. Thread intT1 = new Thread(new Runnable() {
  9. @Override
  10. public void run() {
  11. atomicInt.compareAndSet(100, 101);
  12. atomicInt.compareAndSet(101, 100);
  13. }
  14. });
  15. Thread intT2 = new Thread(new Runnable() {
  16. @Override
  17. public void run() {
  18. try {
  19. TimeUnit.SECONDS.sleep(1);
  20. } catch (InterruptedException e) {
  21. }
  22. boolean c3 = atomicInt.compareAndSet(100, 101);
  23. System.out.println(c3); // true
  24. }
  25. });
  26. intT1.start();
  27. intT2.start();
  28. intT1.join();
  29. intT2.join();
  30. Thread refT1 = new Thread(new Runnable() {
  31. @Override
  32. public void run() {
  33. try {
  34. TimeUnit.SECONDS.sleep(1);
  35. } catch (InterruptedException e) {
  36. }
  37. atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
  38. atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
  39. }
  40. });
  41. Thread refT2 = new Thread(new Runnable() {
  42. @Override
  43. public void run() {
  44. int stamp = atomicStampedRef.getStamp();
  45. try {
  46. TimeUnit.SECONDS.sleep(2);
  47. } catch (InterruptedException e) {
  48. }
  49. boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
  50. System.out.println(c3); // false
  51. }
  52. });
  53. refT1.start();
  54. refT2.start();
  55. }
  56. }

总结

简单说来,在多线程环境中,为了进行性能上的优化,可以设计一些无锁的算法。这里面会需要进行较多的判断,有些判断是十分关键的(比如说CAS中的判断),ABA主要存在这些判断中。有的时候,我们并不只是需要判断变量是不是我们看到的那个值,还需要在执行操作的过程中,判断这个变量是否已经发生了改变。得益于垃圾回收机制,用Java设计无锁算法的时候,可能出现ABA问题的情况还是相对比较少的。

时间: 2024-10-21 09:42:22

传说中的并发编程ABA问题的相关文章

JAVA并发编程J.U.C学习总结

前言 学习了一段时间J.U.C,打算做个小结,个人感觉总结还是非常重要,要不然总感觉知识点零零散散的. 有错误也欢迎指正,大家共同进步: 另外,转载请注明链接,写篇文章不容易啊,http://www.cnblogs.com/chenpi/p/5614290.html 本文目录如下,基本上涵盖了J.U.C的主要内容: JSR 166及J.U.C Executor框架(线程池. Callable .Future) AbstractQueuedSynchronizer(AQS框架) Locks & C

并发编程学习

1,java内存模型(JMM):主内存与工作内存:主内存存储了所有变量,每条线程有自己的工作内存,工作内存保存在被线程使用的变量和主内存变量的副本,线程操作必须在工作内存中进行,不能直接读取主内存而线程间的值传递需要主内存. ,内存操作有8条语句均是原子的. 2,线程同步的方法(多个线程对共享数据的竞争是线程不安全的因素) 线程同步总体可分为有锁同步和无锁同步.有锁同步就是加锁,主要包括synchronized,lock,信号量:无锁同步主要包括CAS,MVCC(参见数据库),volatile,

《Java并发编程实战》第十五章 原子变量与非阻塞同步机制 读书笔记

一.锁的劣势 锁定后如果未释放,再次请求锁时会造成阻塞,多线程调度通常遇到阻塞会进行上下文切换,造成更多的开销. 在挂起与恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断. 锁可能导致优先级反转,即使较高优先级的线程可以抢先执行,但仍然需要等待锁被释放,从而导致它的优先级会降至低优先级线程的级别. 二.硬件对并发的支持 处理器填写了一些特殊指令,例如:比较并交换.关联加载/条件存储. 1 比较并交换 CAS的含义是:"我认为V的值应该为A,如果是,那么将V的值更新为B,否则不需要修

java并发编程11.原子变量与非阻塞同步机制

在非阻塞算法中不存在死锁和其他活跃性问题. 在基于锁的算法中,如果一个线程在休眠或自旋的同时持有一个锁,那么其他线程都无法执行下去,而非阻塞算法不会受到单个线程失败的影响. 锁的劣势 许多JVM都对非竞争锁获取和释放操作进行了极大的优化,但如果有多个线程同时请求锁,那么JVM就需要借助操作系统地功能.如果出现了这种情况,那么一些线程将被挂起并且在稍后恢复运行.当线程恢复执行时,必须等待其他线程执行完它们的时间片以后,才能被调度执行.在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较大时间

Java并发编程深入学习

基本概念 在实践中,为了更好的利用资源提高系统整体的吞吐量,会选择并发编程.但由于上下文切换和死锁等问题,并发编程不一定能提高性能,因此如何合理的进行并发编程时本文的重点,接下来介绍关于锁最基本的一些知识(选学). volatile:轻量,保证共享变量的可见性,使得多个线程对共享变量的变更都能及时获取到.其包括两个子过程,将当前处理器缓存行的数据写回到系统内存,之后会使其他CPU里缓存了该内存地址的数据无效. synchronized:相对重量,其包含3种形式,针对普通同步方法,锁是当前实例对象

Java并发编程实战(中文版)pdf

下载地址:网盘下载 内容简介  · · · · · · 本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册.书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险.构造线程安全的类及验证线程安全的规则,如何将小的线程安全类组合成更大的线程安全类,如何利用线程来提高并发应用程序的吞吐量,如何识别可并行执行的任务,如何提高单线程子系统的响应性,如何确保并发程序执行预期任务,如何提高并发代码的性能和可伸缩性等内容,最后介绍了一些高级主题,

干货:Java并发编程必懂知识点解析

本文大纲 并发编程三要素 原子性 原子,即一个不可再被分割的颗粒.在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败. 有序性 程序执行的顺序按照代码的先后顺序执行.(处理器可能会对指令进行重排序) 可见性 当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值. 2. 线程的五大状态 创建状态 当用 new 操作符创建一个线程的时候 就绪状态 调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度

干货:Java并发编程必懂知识点解析(内附面试题)

本文大纲 1.并发编程三要素 原子性 原子,即一个不可再被分割的颗粒.在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败. 有序性 程序执行的顺序按照代码的先后顺序执行.(处理器可能会对指令进行重排序) 可见性 当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值. 2. 线程的五大状态 创建状态 当用 new 操作符创建一个线程的时候 就绪状态 调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的

Java并发编程75个问答

1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on):true则把该线程设置为守护线程,反之则为用户线程.Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常. 两者的区别: 唯一的区别是判断虚拟机(JVM)何时离开,Daemon是为其他线程提供服务,如果全部的User Thread已经