在《JAVA并发编程实战》的第15.4.4节中看到了一些关于ABA问题的描述。​​有一篇文章​​摘录了书里的内容。

书中有一段内容为:

如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现ABA问题。在这种情况下,即使链表的头结点仍然只想之前观察到的节点,那么也不足以说明链表的内容没有发生变化。如果通过垃圾回收器来管理链表节点仍然无法避免ABA问题,那么还有一个相对简单的解决方法:不是只是更新某个引用的值,而是更新两个值,包含一个引用和一个版本号。

这一段说到了“如果采用自己的方式管理节点对象的内存,可能出现ABA问题”,又说通过垃圾回收器来管理链表节点可能避免ABA问题。但是这些表述太简略,让我有些困惑。具体怎么样管理内存,会出现ABA问题?GC会什么可能会避免ABA问题?为什么只是“可能会避免”?

在JDK的ConcurrentLinkedQueue的源码注释中,有以下说法:

* This is a modification of the Michael & Scott algorithm,
* adapted for a garbage-collected environment, with support for
* interior node deletion (to support remove(Object)). For
* explanation, read the paper.
*
* Note that like most non-blocking algorithms in this package,
* this implementation relies on the fact that in garbage
* collected systems, there is no possibility of ABA problems due
* to recycled nodes, so there is no need to use "counted
* pointers" or related techniques seen in versions used in
* non-GC'ed settings.

 

里边有两点要注意的:

  1. 说这个类的实现是对"Michael & Scott"算法的一个修改,这个修改是基于ConcurrentLinkedList的实现存在于“垃圾回收的环境”。
  2. 说这个类的实现依赖于“在GC系统中,there is no possibility of ABA problems due to recycled nodes"。问题在于,什么叫”recycled nodes”?


 

好吧,那么在JAVA中怎么操作可能会现ABA问题呢?

假如有一个Queue(先别管它怎么实现),那么在以下情况下会出现ABA问题:

  1. 我们在CAS中比较对节点的引用 & 我们复用节点。假如queue初始的状态是A -> E。在变化后的状态是A -> X -> E。那么我们在CAS中比较对A的引用时,就无法看出状态的变化。“复用”,就是像这个例子一样,把同一个节点再次加个队列。
  2. 我们在CAS中比较对节点的引用 & 某个new出来的节点A2的地址恰好和A1的地址相同。

第一种情况不管GC环境还是非GC环境,都会造成ABA问题。所以GC只是可能会避免ABA问题,就像《JAVA并发编程实战》中说的一样。

GC环境和无GC的环境(如C++)的不同在于第二种情况。即,在JAVA中第,第二种情况是不可能发生的。原因在于,在我们用CAS比较A1和A2这两个引用时,暗含着的事实是——这两个引用存在,所以它们所引用的对象都是GC root可达的。那么在A1引用的对象还未被GC时,一个新new的对象是不可能和A1的对象有相同的地址的。所以A1 != A2。

所以,在JAVA的GC环境中,如果两个引用在CAS中被判断为相等,它们引用的肯定是同一个对象。但是,这种描述对于非GC环境不成立。

例如,在C++中,我们对指针(类比于JAVA中的引用)采用CAS操作,那么即使两个指针相同,他们也未必引用同一个对象,有可能是第一个指针所指向的内存被释放后,第二个对象恰好分配在相同地址的内存。

在维基百科上,给出了上面提到的第一种情况在​​C++中的一个例子​​。在这个例子中,复用节点的行为,可能会导致使用一个悬垂指针。

同时,维基百科也对第二种情况给出了描述:

A common case of the ABA problem is encountered when implementing a ​​lock-free​​ data structure. If an item is removed from the list, deleted, and then a new item is allocated and added to the list, it is common for the allocated object to be at the same location as the deleted object due to optimization. A pointer to the new item is thus sometimes equal to a pointer to the old item which is an ABA problem.



在ConcurrentLinkedList的实现中,并不存在复用节点的行为。在这个类的实现内部,以及它提供给用户的API,都无法使得节点被复用,而且这是JAVA环境中。所以ConcurrentLinkedList的实现中直接对node的引用进行CAS操作,而不必担心ABA问题。

例如,在offer的实现中(JDK1.7)




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17


18


19


20


21


22


23


24


25


26


27


28


29




​public​​ ​​boolean​​ ​​offer(E e) {​


​checkNotNull(e);​


​final​​ ​​Node<E> newNode = ​​​​new​​ ​​Node<E>(e);​


 


​for​​ ​​(Node<E> t = tail, p = t;;) {​


​Node<E> q = p.next;​


​if​​ ​​(q == ​​​​null​​​​) {​


​// p is last node​


​if​​ ​​(p.casNext(​​​​null​​​​, newNode)) {​


​// Successful CAS is the linearization point​


​// for e to become an element of this queue,​


​// and for newNode to become "live".​


​if​​ ​​(p != t) ​​​​// hop two nodes at a time​


​casTail(t, newNode);  ​​​​// Failure is OK.​


​return​​ ​​true​​​​;​


​}​


​// Lost CAS race to another thread; re-read next​


​}​


​else​​ ​​if​​ ​​(p == q)​


​// We have fallen off list.  If tail is unchanged, it​


​// will also be off-list, in which case we need to​


​// jump to head, from which all live nodes are always​


​// reachable.  Else the new tail is a better bet.​


​p = (t != (t = tail)) ? t : head;​


​else​


​// Check for tail updates after two hops.​


​p = (p != t && t != (t = tail)) ? t : q;​


​}​


​}​