簡書 賈小強
轉載請注明原創出處,謝謝!
一個Java 5中最好的補充是對原子操作的支持類,如AtomicInteger
,AtomicLong
等。這些類幫助你減少復雜的(不必要的)多線程代碼,實際上只是完成一些基本操作,如增加或減少多個線程之間的共享的值。這些類在內部依賴于一個名為CAS(Compare and Swap)的算法。在這篇文章中,我將詳細討論這個概念。
樂觀悲觀鎖 (Optimistic and Pessimistic Lock)
傳統的鎖機制,例如在Java中使用的synchronized關鍵字,是多線程的悲觀鎖技術。它要求您首先保證沒有其他線程將在某些操作之間進行干擾(即鎖定對象)。
這就像是說:“請先把門關上,否則會有其他的騙子進來整理你的東西”。
雖然上面的方法是安全的,它確實有效,但它在性能上對應用程序造成了嚴重的影響。原因很簡單,等待線程不能做任何事情,除非它們也有機會執行被保護的操作。
實際上存在一種更有效性能更好的方式,在本質上是樂觀的。在這種方法中,您繼續進行更新,希望您能在不受干擾的情況下完成。這種方法依賴于碰撞檢測,來確定是否更新時被其他方面的干擾,在這種情況下,操作失敗可以重試(或不)。
樂觀的方法就像一句老話:“獲得原諒比獲得許可更容易”,在這里“更容易”意味著“更有效”。
Compare and Swap是這種樂觀方法的一個很好例子,我們將在下面討論。
Compare and Swap算法
該算法將某個內存位置的內容與給定值進行比較(compare),只有當它們相同時,才將內存位置的內容修改為給定的新值。這是作為單個原子操作完成的。原子性保證了新值是在最新值基礎上進行計算的;如果該值已被另一個線程同時更新,那么修改會失敗。操作的結果必須指明它是否執行替換(swap),這可以通過簡單的布爾響應(這個變體通常稱為compare-and-set),或者返回從內存位置讀取的值(而不是寫入它的值)來完成。
比如CAS操作有3個參數:
- 一個內存位置V,其值必須被替換。
- 上一次線程讀的舊值A。
- 應在V上寫的新值B。
CAS說:“我覺得V應該有值A;如果是,把B放在那里,否則不要改變它,但告訴我,我錯了。”CAS樂觀的希望的更新成功,同時如果另一個線程從它上次檢查后更新了變量,它會檢測到失敗。
讓我們用一個例子明白整個過程。假設V是存儲值“10”的內存位置。有多個線程希望增加這個值,并將遞增的值用于其他操作。讓我們逐步完成整個CAS操作:
- 初始狀態。
V = 10, A = 0, B = 0 - 現在線程1首先出現,并將V與它的上次讀取的值A進行比較(compare):
V = 10, A = 10, B = 11
if A = V
V = B
else
operation failed
return V
顯然,V的值將被替換(swap)為11,即操作成功。
- 然后線程2執行與線程1相同的操作,這個時候內置位置V實際上值已經是11,但線程2上次讀取的還是10。
V = 11, A = 10, B = 11
if A = V
V = B
else
operation failed
return V
- 在這種情況下,將V與它的上次讀取的值A進行比較(compare),V不等于A,所以不替換值,返回V,而即當前值11。現在線程2,再次用值重試(retry)這個操作:
V = 11, A = 11, B = 12
此時,條件滿足并將值遞增為12,然后返回給線程2。
總之,當多個線程嘗試使用CAS同時更新同一個變量時,其中一個線程會成功更新變量的值,剩下的會失敗。但失敗者不會受到線程的懲罰。他們可以自由地重試操作或干脆什么也不做。
這樣每個線程對值得增加都是實打實的增加了,而不會導致線程1,線程2同時讀取V為10,然后線程1增加為11,線程2也自認為增加了,但結果還是11,通過CAS算法避免了消失的遞增,解決了明明線程2任務執行了,計數器卻沒計數的悲劇**
這就是有關Java的原子(atomic)操作簡單但重要的概念。
Happy Learning !!