標籤:
轉自:http://www.importnew.com/11345.html
我只是喜歡新鮮的事物,而Java 8 有很多新東西。這次我想討論其中我最喜歡的之一:並發加法器。這是一個新的類集合,他們用來管理被多線程讀寫的計數器。這個新的API在顯著提升效能同時,仍然保持了簡單直接的特點。
多核架構到來之後人們就解決著並發計數器,讓我們來看看到現在為止Java提供了哪些解決並發計數器的選項,並對比一下他們與新API的效能。
髒計數器 – 這種方法意味著一個常規對象或靜態屬性正在被多線程讀寫。不幸的是,由於兩個原因這行不通。原因之一,在Java中A += B操作不是原子的。如果你開啟輸出位元組碼,你將至少看到四個指令 —— 第一個用來將屬性值從堆載入到線程棧,第二個用來載入delta,第三個用來把它們相加,第四個用來將結果重新分配給屬性值。
如果多個線程同時作用於同一塊記憶體單元,寫操作有很大機會丟失,因為一個線程可以覆蓋另一個線程的值(又名“讀-修改-寫”),另一個令人不快的是這種情況下你不得不處理值的衝突,還有更壞的情況。
這是相當菜鳥的一個問題,而且超級難調試。如果你確實發現有人在你的應用中這麼做的話,我想要你幫個小忙。在你的資料庫中搜尋“Tal Weiss”,如果存在我的記錄,請刪除,這樣我會覺得安全些。
Synchronized – 最基本的並發用語,它在讀寫一個值的時候會阻塞所有想讀寫該值的其他線程。雖然它是可行的,但你的代碼卻註定要被轉向DMV line。
讀寫鎖 – 基本Java鎖的略複雜版本,它使你能夠區分修改值並且需要阻塞其他線程的線程和僅是讀取值並且不需要臨界區的線程。雖然這更有效率(假設寫線程數量很 少),但由於當你擷取寫鎖的時候阻塞了所有其他線程的執行,這真是一個“漂亮”的方法。事實上,只有當你瞭解到相比讀線程,寫線程的數量極大地受限時它才 真正是一個好方法。
Volatile – 這個關鍵詞非常容易被誤解,它指示JIT編譯器重新最佳化運行時機器碼,使得屬性的任何修改對其他線程都是即時可見的。
這將導致一些JIT處理記憶體配置的順序這項JIT編譯器最喜愛的最佳化失效。你再說一遍?是的,你沒有聽錯。JIT編譯器可以改變屬性分配的順序。這個神秘的小策略(又叫happens-before)能夠最小化程式訪問全域堆的次數,同時仍然確保你的代碼沒有被影響。真是相當隱蔽…
所以什麼時候應該使用volatile處理計數器呢?如果你僅有一個線程更新值並且多個線程讀取它,這時使用volatile無疑是一個真正好的策略。
那為什麼不總是使用它呢?因為當多個線程同時更新屬性的時候它不能很好的工作。由於A += B不是原子操作,這將帶來覆蓋其他寫操作的風險。在Java8之前,處理這種情況你需要使用的是AtomicInteger。
AtomicInteger – 這組類使用CAS(比較並交換)處理器指令來更新計數器的值。聽起來不錯,真的是這樣嗎?是也不是。好的一面是它通過一個直接機器碼指令設定值時,能夠最 小程度地影響其他線程的執行。壞的一面是如果它在與其他線程競爭設定值時失敗了,它不得不再次嘗試。在高競爭下,這將轉化為一個自旋鎖,線程不得不持續嘗 試設定值,無限迴圈直到成功。這可不是我們想要的方法。讓我們進入Java 8的LongAdders。
Java 8 加法器 – 這是一個如此酷的新API以至於我一直在滔滔不絕地談論它。從使用的角度看它與AtomicInteger非常相似,簡單地建立一個LongAdder執行個體,並使用intValue()和add()來擷取和設定值。神奇的地方發生在幕後。
這個類所做的事情是當一個直接CAS由於競爭失敗時,它將delta儲存在為該線程分配的一個內部單元對象中,然後當intValue()被調用時,它會將這些臨時單元的值再相加到結果和中。這就減少了返回重新CAS或者阻塞其他線程的必要。多麼聰明的做法!
好吧,已經說的夠多了-讓我們看看這個類的實際表現吧。我們設立了下面的基準測試-通過多線程將一個計數器增加到10^8。我們用總共10個線程來運行這個測試-5個寫操作,5個讀操作。測試機器僅有一個四核的i7處理器,因此測試一定會產生一些嚴重的競爭:
代碼在這裡可以下載到
注意dirty和volatile都冒著一些嚴重的值覆蓋危險。
總結
- 並行加法器相比原子整數擁有60%-100%的效能提升
- 執行加法的線程之間沒有太大差別,除非被鎖定
- 注意當你使用synchronized或讀寫鎖時所帶來的巨大效能問題 – 慢一個甚至兩個數量級
我非常願意聽到-你已經有機會在你的代碼中使用這些類了。
Java 8 LongAdders:管理並發計數器的正確方式