標籤:
前言
在Java並發包中有這樣一個包,java.util.concurrent.atomic,該包是對Java部分資料類型的原子封裝,在原有資料類型的基礎上,提供了原子性的操作方法,保證了安全執行緒。下面以AtomicInteger為例,來看一下是如何?的。
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; }}
public final int decrementAndGet() { for (;;) { int current = get(); int next = current - 1; if (compareAndSet(current, next)) return next; }}
以這兩個方法為例,incrementAndGet方法相當於原子性的++i,decrementAndGet方法相當於原子性的--i(根據第一章和第二章我們知道++i或--i不是一個原子性的操作),這兩個方法中都沒有使用阻塞式的方式來保證原子性(如Synchronized),那它們是如何保證原子性的呢,下面引出CAS。
Compare And Swap
CAS 指的是現代 CPU 廣泛支援的一種對記憶體中的共用資料進行操作的一種特殊指令。這個指令會對記憶體中的共用資料做原子的讀寫操作。簡單介紹一下這個指令的操作過程:首先,CPU 會將記憶體中將要被更改的資料與期望的值做比較。然後,當這兩個值相等時,CPU 才會將記憶體中的數值替換為新的值。否則便不做操作。最後,CPU 會將舊的數值返回。這一系列的操作是原子的。它們雖然看似複雜,但卻是 Java 5 並發機制優於原有鎖機制的根本。簡單來說,CAS 的含義是“我認為原有的值應該是什麼,如果是,則將原有的值更新為新值,否則不做修改,並告訴我原來的值是多少”。(這段描述引自《Java並發編程實踐》)
簡單的來說,CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。若且唯若預期值A和記憶體值V相同時,將記憶體值V修改為B,否則返回V。這是一種樂觀鎖的思路,它相信在它修改之前,沒有其它線程去修改它;而Synchronized是一種悲觀鎖,它認為在它修改之前,一定會有其它線程去修改它,悲觀鎖效率很低。下面來看一下AtomicInteger是如何利用CAS實現原子性操作的。
CAS的ABA問題
所謂 ,問題基本是這個樣子:
- 進程P1在共用變數中讀到值為A
- P1被搶佔了,進程P2執行
- P2把共用變數裡的值從A改成了B,再改回到A,此時被P1搶佔。
- P1回來看到共用變數裡的值沒有被改變,於是繼續執行。
雖然P1以為變數值沒有改變,繼續執行了,但是這個會引發一些潛在的問題。ABA問題最容易發生在lock free 的演算法中的,CAS首當其衝,因為CAS判斷的是指標的地址。如果這個地址被重用了呢,問題就很大了。(地址被重用是很經常發生的,一個記憶體配置後釋放了,再分配,很有可能還是原來的地址)
比如上述的DeQueue()函數,因為我們要讓head和tail分開,所以我們引入了一個dummy指標給head,當我們做CAS的之前,如果head的那塊記憶體被回收並被重用了,而重用的記憶體又被EnQueue()進來了,這會有很大的問題。(記憶體管理中重用記憶體基本上是一種很常見的行為)
這個例子你可能沒有看懂,維基百科上給了一個活生生的例子——
你拿著一個裝滿錢的手提箱在飛機場,此時過來了一個火辣性感的美女,然後她很暖昧地挑逗著你,並趁你不注意的時候,把用一個一模一樣的手提箱和你那裝滿錢的箱子調了個包,然後就離開了,你看到你的手提箱還在那,於是就提著手提箱去趕飛機去了。
這就是ABA的問題。
java多線程之CAS