在《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.
裡邊有兩點要注意的:
- 說這個類的實現是對"Michael & Scott"演算法的一個修改,這個修改是基於ConcurrentLinkedList的實現存在於“垃圾回收的環境”。
- 說這個類的實現依賴於“在GC系統中,there is no possibility of ABA problems due to recycled nodes"。問題在於,什麼叫”recycled nodes”?
好吧,那麼在JAVA中怎麼操作可能會現ABA問題呢?
假如有一個Queue(先別管它怎麼實現),那麼在以下情況下會出現ABA問題:
- 我們在CAS中比較對節點的引用 & 我們複用節點。假如queue初始的狀態是A -> E。在變化後的狀態是A -> X -> E。那麼我們在CAS中比較對A的引用時,就無法看出狀態的變化。“複用”,就是像這個例子一樣,把同一個節點再次加個佇列。
- 我們在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)
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; } }
用if (p.casNext(null, newNode))來確保只有p是尾節點時,才將新結點連結在p的後邊。