`

转:并发编程中ABA问题

阅读更多

什么是ABA问题

转发地址:http://www.lantaozi.com/article/521b199f0ff2456b5b000001

 

ABA并不是一个缩写,更像是一个形象的描述。ABA问题出现在多线程或多进程计算环境中。该问题Wiki上有详细的介绍,本文将进行更通俗的演绎该问题。

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

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

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

现实ABA问题

Wiki百科上有个形象的现实的例子,这里可以简单用两个字来概括下:掉包。

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

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

程序世界的ABA问题

在程序的世界里,ABA是什么样呢?讨论ABA问题之前,先来讨论一种操作——CAS操作。

在多线程操作环境中,如果某些操作不是原子的,那么一般如果想让它变成原子的,那么就需要采用一些同步的方法,加个锁什么的。比如说,++就不是一个原子的操作(虽然只有一行)。我们还知道,有一种Atomic原子类,它可以原子地实现我们直观上认为的非原子操作。而且观察它的源码你会发现,它并没有使用锁这样的方式。那么Atomic是怎么原子地来进行操作的呢?看下面的代码:

    public final int getAndAdd( int delta) {

        for (;;) {

            int current = get();

            int next = current + delta;

            if (compareAndSet(current, next))

                return current;

        }

    }

这是AtomicInteger中的一个方法:获取并增加delta值量。这里用来保证线程安全的是compareAndSet操作,也就是传说中的CAS操作了。如果CAS操作没有成功地话,那么会重新去获取最新的valuevolatile用来保证线程可见性),重新执行一遍,直到成功为止。这里使用CAS乐观锁,而不是synchronized悲观锁。

1.    CAS乐观锁算法相对于synchronized的操作显然有不少的优点,不会出现死锁等活跃性问题

2.    为什么CAS操作能保证原子性?这得益于底层硬件的支持,硬件上保证了操作是线程安全的。

大多数乐观锁算法都是基于CAS操作的,因为没有锁定操作,当进行一些重要操作的时候,就需要检查相关的状态有没有被其他线程改变,比如JUC中并发集合(Collections)等。在状态进行检查比较过程中,就可以出现ABA问题。不过,上面的CAS显然不会存在ABA问题。

在实现一个复杂的数据结构的时候,比如说并发集合(初始一个元素A)进行操作的时候,如push一个新元素B。在并发中,加锁实现的时候,可以保证push过程中,栈的元素不会被别的线程改变。但是,CAS实现的时候,可能会存在如下的代码片段:

           oldHead = top .get();           //1

           newHead = oldHead .next()       //2

           top.CAS(oldHead , newHead);//3

如果执行到第1行结束的时候,另一个线程pop了原来的topA,然后push一个新的元素C,然后又把A push进去了。结构变成了A->C->A。但是这个线程还以为是top->A,继续执行后变成了top->B->A->C。至于之后会发生什么问题,就要根据实际情况查看了。

实际上,CAS对一个地址的操作更容易出现ABA问题。也就是现实ABA例子中,对箱子操作,而不是箱子里的内容进行操作。在C这些需要自己管理内存的语言中,容易诱发严重的问题。有一定C基础的,也可以参考[3]。其实ABA问题不一定只与CAS操作有关系。

Java中的例子

Jdkconcurrent包中就有对ABA问题的处理,比如说,CocurrentHashMap中的处理。ConcurrentHashMap中的某些操作使用了半无锁的处理方式。简单来说,就是先用无锁的算法理念进行处理,如果有问题,那么就强制锁定每个切片(ConcurrentHashMap将元素hash到不同的切片中),从而确保操作正确性。

比如isEmpty的实现,这里并没有锁定segments里的每个切片:

    public boolean isEmpty() {

        final Segment<K,V>[] segments = this. segments;

        /*

         * We keep track of per-segment modCounts to avoid ABA

         * problems in which an element in one segment was added and

         * in another removed during traversal, in which case the

         * table was never actually empty at any point. Note the

         * similar use of modCounts in the size() and containsValue()

         * methods, which are the only other methods also susceptible

         * to ABA problems.

         */

        int[] mc = new int[segments.length];

        int mcsum = 0;

        for (int i = 0; i < segments. length; ++i) {

            if (segments[i]. count != 0)

                return false;

            else

                mcsum += mc[i] = segments[i]. modCount;

        }

        // If mcsum happens to be zero, then we know we got a snapshot

        // before any modifications at all were made.  This is

        // probably common enough to bother tracking.

        if (mcsum != 0) {

            for ( int i = 0; i < segments. length; ++i) {

                if (segments[i]. count != 0 ||

                    mc[i] != segments[i]. modCount)

                    return false;

            }

        }

        return true;

    }

这里的ABA问题是什么?就是在并发的环境中,当执行到if (mcsum != 0) { 行,现在的状态是A(所有的切片中都是空的),在线程环境中,如果不加锁,这是不靠谱的,我们还是再判断一遍,保证这段时间内,切片中还是空的。如果我们不考虑mode检查(忽略所有与modCount相关的代码),那么在第二遍检查的时候,如果所有的切片中还是空的(A),那么我们是否可以直接返回true呢?这是不对的,我们不知道有没有别的线程在我们检查同一切片的过程中插入了一个元素(B),然后又删除了(A)。那么如果存在这样的情况,我们是不是应该返回true呢?好吧,并发环境中,实在不好严格地判断。实际上,这样是被判false的。

它是怎么解决ABA问题的呢?第一个循环中,分别检查每个切片是否为空的,如果不是,直接返回false,如果是,那么就记录每个切片的modCount(每次改变元素数量的时候+1)并做一个检查和mcsum。如果执行到判断mcsum是否为0,就表示在之前的循环中,所有的切片中的元素在该线程中被判断的时候,是为空的。至于在检查前后是否被修改未知(多线程环境中,即使同一时间点不是所有的切片中都为空的,最后的结果也可能是空的,因为检查不是在同一个时间点进行的)。

如果最后的mcsum0modCount的初始值为0),表示还没有操作过这个map,就为空的,这个没有问题。

mcsum不为0的时候,再次检查每个切片中元素的数量,如果不为0,显然就不为空了,否则,看modCount是否和之前记录的mc的是否相等,不等表示有改变(添加或删除元素),如果有改变,那么也就返回false了。

按照加锁的思维,我们判断isEmpty的时候只要把所有是切片都锁定,然后挨个统计数量就好了(事实上,size方法中有这样的操作,就是在尝试不加锁的情况下,有问题的时候,就会进行锁定),但是这样的话,判断是否为空的时候,其他的线程就可能阻塞。不加锁是对性能上的优化。

modCount就是解决ABA问题中思路的体现。每次操作元素的时候,不止更新切片中的元素列表,同时维护一个修改的版本(modCount),如果修改就加1,那么,通过检查这个版本就可以知道是否有被修改过。

思考:Segment中为什么countvolatile的,但是modCount不是呢?

Java中解决ABA

Java中对于ABA问题的解决,提供下面两种方法(主要是跟踪每次修改,并记录变化):AtomicMarkableReference/AtomicStampedReference具体可以查看相关API

总结

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

得益于垃圾回收机制,用Java设计无锁算法的时候,可能出现ABA问题的情况还是相对比较少的。

[参考]

1.    Java并发编程实践 Ch15

2.    wiki百科ABA 

3.    无锁队列的 

分享到:
评论

相关推荐

    汪文君高并发编程实战视频资源下载.txt

     高并发编程第三阶段07讲 AtomicReference详解,CAS算法带来的ABA问题详解.mp4  高并发编程第三阶段08讲 AtomicStampReference详解,解决CAS带来的ABA问题.mp4  高并发编程第三阶段09讲 AtomicIntegerArray,...

    汪文君高并发编程实战视频资源全集

     高并发编程第三阶段07讲 AtomicReference详解,CAS算法带来的ABA问题详解.mp4  高并发编程第三阶段08讲 AtomicStampReference详解,解决CAS带来的ABA问题.mp4  高并发编程第三阶段09讲 AtomicIntegerArray,...

    Java并发编程全景图.pdf

    并发编程中常见问题包括死锁(DeadLock)、活锁(LiveLock)、优先级倒置(PriorityInversion)等。这些问题可能导致程序的性能下降或者死锁。解决这些问题的方法包括使用定时锁、避免循环等待等。 10. 原子操作和...

    Java高并发编程详解.md

    ```AtomicStampedReference```来解决ABA问题,类中的```compareAndSet```方法作用首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果相等,以原子方式将该引用和标记的值设置为给定的更新值。

    Java并发编程实战

    Java并发编程实战 本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及...

    面试必问并发编程高级面试专题.zip

    在面试中,对于高级职位的候选人,面试官常常会深入探讨并发编程的相关知识点,以评估候选人的技术水平和问题解决能力。下面,我们将详细探讨一些并发编程的高级主题。 1. **线程与进程**:在操作系统中,线程是...

    并发编程之一 日常学习笔记

    线程安全是并发编程中的核心问题。Java提供了多种保证线程安全的方式,包括线程局部变量(ThreadLocal)、volatile关键字以及前面提到的原子类。线程局部变量保证了每个线程都有一份自己的副本,不会产生数据竞争;...

    【并发编程】CAS到底是什么.pdf

    ### 并发编程之CAS详解 #### 一、并发编程基本概念 在开始探讨CAS之前,我们首先简要回顾一下并发...这对于初学者、中级开发者甚至是高级开发者来说都是非常宝贵的资源,有助于他们在实践中更好地应用并发编程技术。

    浅谈Java中ABA问题及避免

    ABA问题是Java并发编程中的一种常见问题,需要开发者对其进行认真对待和处理,以避免程序的不正确执行和数据的不一致。 相关知识点: * ABA问题的定义和后果 * ABA问题的产生原因 * 如何避免ABA问题 * Java中的锁...

    并发编程 70 道面试题及答案.docx

    并发编程知识点大全 并发编程是指多个线程同时访问和修改共享资源的编程方式。为了避免并发编程中的各种问题,...并发编程是 Java 编程中的一个重要方面,需要掌握多种同步机制和锁机制来避免并发编程中的各种问题。

    深入浅出_Java并发工具包原理讲解

    通过J.U.C,Java开发者可以更加高效地解决并发编程中遇到的问题。但同时,也要注意J.U.C中的一些设计思想和使用原则,比如正确使用锁,理解CAS操作可能带来的ABA问题,以及对不同并发工具类适用场景的把握。这需要...

    3、并发编程之CAS&amp;Atomic原子操作详解.pdf

    这一特性在并发编程中尤为重要,因为它能够帮助开发者有效地处理多线程环境下的数据一致性问题。 #### 二、原子性的定义 原子性是事务处理的关键属性之一,通常指的是事务的不可分割性。在事务处理中,事务包含的...

    并发编程atomic&collections-课上笔记1

    本文主要讲述了 Java 中的并发编程,包括 atomic 包的介绍、CAS 算法的原理、ABA 问题的解决方案,以及 collections 中的 HashMap、HashTable 和 ConcurrentHashMap 的源码分析。 Atomic 包的介绍 ----------------...

    Java并发编程实战(中文版带详细目录).pdf

    ### Java并发编程实战知识点概述 #### 一、Java并发编程基础 **1.1 并发与并行的概念** ...通过学习这些概念和技术,可以深入理解Java并发编程的核心原理,并能够在实际开发中灵活运用这些技术解决复杂的并发问题。

    并发编程——原子操作CAS.pdf

    本文档详细介绍了并发编程中的原子操作,特别是Java语言中通过CAS(Compare-And-Swap)实现的原子操作,并指出了在实际编程中如何使用和实现原子操作。 首先,文档开篇就介绍了原子操作的定义。所谓原子操作,指的...

    中等规模的并发程序设计.ppt

    **死锁和避免策略**:死锁是并发编程中的常见问题,由ABA问题和其他因素引起。解决死锁的方法包括预防协议、检测恢复和忽略死锁的存在。 **程序分解**:并发设计中,任务分解、数据分解和数据流分解是提高效率的...

    concurrency:并发学习

    并发编程 项目初始化 线程安全性 原子性-Atomic包 AtomicXXX:CAS、Unsafe.compareAndSwapInt AtomicLong、LongAdder AtomicReference、AtomicReferenceFieldUpdater AtomicStampReference:CAS的ABA问题 原子性-...

    Java并发编程面试题(2022最新版)

    ### Java并发编程面试题知识点详解 #### 一、基础知识 **并发编程的优缺点** - **优点:** - **提高资源利用率:** 并发可以让多个任务共享资源,提高CPU和其他硬件资源的利用率。 - **提升系统响应速度:** 多...

    java自带并发框架

    Java并发框架是Java JDK中内置的一系列用于处理多线程并行执行的工具和类库,自JDK 5.0引入以来,极大地简化了并发编程的复杂性。这个框架由Doug Lea设计,并在JSR-166任务中提出,最终在Tiger(JDK 5)版本中被引入...

Global site tag (gtag.js) - Google Analytics