我喜歡新鮮玩意兒,而Java 8裏面就有很多。這回我準備介紹一下個人一個最愛——併發計數器。這是一組新的類,用於維護多個線程併發讀寫的計數器。新的API帶來了顯著的性能提高,同時還保證了接口的簡單易用。git
多核時代來臨了以後,你們都開始使用併發計數器,咱們先來看一下Java迄今爲止提供了哪些實現方式,它們的性能和這個新的API相比,又有什麼不一樣。github
髒計數器——選擇這種方式意味着多線程直接併發讀寫一個普通對象或者靜態字段。不幸的是,這麼作是行不通的。有兩個緣由,一個是在Java裏, A+= B操做不是原子的。若是你打開編譯後的字節碼看一下,你會發現至少有四條指令——第一條是從堆裏將字段值加載到線程棧裏,第二條是加載要增長的值,第三條指令將它們進行相加,第四條則將結果寫回到字段中。數據庫
若是多個線程同時在同一個內存位置進行這個操做,你的一個寫操做頗有可能就丟掉了,由於另外一個線程可能會覆蓋了它的值。還有一個很噁心的事就是這個值的可見性。下面還會詳細介紹到。小程序
新手很是容易犯這樣的錯誤,而這樣的問題卻很難發現。若是你發現團隊中有人這麼作,最好能幫我個小忙。在你的數據庫裏面搜一下個人名字「Tal Weiss"。若是你發現我在裏面——趕忙把個人記錄刪掉。這樣我會感受舒服點。多線程
synchronzied——這是最基礎的同步操做了,只要你在讀寫值,它就會阻塞住其它的全部線程。這種方式的確行得通,不過確定的是,你的程序運行起來會像DMV排的長隊那樣。併發
讀寫鎖(RWLock)——這個和基礎的Java鎖相比就巧妙了些,它可讓你區分出那些要修改值所以須要阻塞別人的進程以及那些只是讀取值不須要進入臨界區的。雖然這個方法有的時候很高效(好比寫線程的數量比較少的話),但仍是至關無語,由於當你獲取寫鎖的時候仍是會阻塞住其它線程的執行。高併發
volatile——這個常常會被誤用的關鍵字會讓JIT編譯中止在運行時進行機器碼的優化工做,所以字段一旦有更新別的線程立刻就能看到。性能
它會使得JIT編譯器常常玩的一些把戲好比說調整賦值語句的順序這些沒法進行。JIT編譯器有可能會改變字段的賦值順序。什麼,你再說一遍?是的,你聽的沒錯。這個神祕的小把戲使得它能夠減少程序訪問全局堆的次數,同時它還能保證不會影響到你的程序的執行。這真是有點偷偷摸摸的感受。測試
那何時應該使用volatile計數器?若是你只有一個線程在更新一個值,而多個線程在讀的話,這是個很合適的場景。由於徹底沒有競爭。優化
你可能會問爲何都使用它就完了?由於若是有多個線程在更新的話就會有問題了。因爲A+=B不是一個原子操做,這麼作的話可能會覆蓋掉別人寫的話。在Java 8之前,這種狀況你就只能用AtomicInteger了。
AtomicInteger——這組類使用了處理器的CAS (compare-and-swap)指令來更新計數器的值。聽起來不錯吧?一半一半吧。因爲它直接使用機器指令來設置值,所以對其它線程的影響最小。很差的一面是若是它和別的線程有競爭賦值失敗了,它會繼續重試。在高併發的條件 下,這就成了一個自旋鎖,線程會在一個無限的循環內不斷的嘗試賦值,直到成功爲止。咱們可不太想看到這種局面。Java 8來了,還帶來了LongAdders。
Java 8 Adders——這是個很是棒的新的API,我對它的仰慕有如滔滔江水連綿不絕。從使用者的角度來講,它很像AtomicInteger。只須要建立一個LongAdder對象,而後使用intValue()以及add()方法來獲取和設置它的值。而奇蹟就發生在這一切的背後。
若是因爲競爭這個類的CAS操做失敗了的話,它會要添加的值存到一個線程本地的內部的cell對象裏。當intValue()方法調用 的時候,它把這些cell的值加到總和裏。這樣就減小了CAS重試或者阻塞別的線程的狀況。真不錯的想法。
說的也差很少了。咱們來看看它的真本事。咱們作了以下的一個基準測試:把一個計數器設置爲0,而後多個線程開始讀取並進行自增。當計數器到達10^8的時候中止。咱們在一個4核的i7處理器上運行這個測試。
我用了10個線程來運行這個基準測試——讀寫分別使用5個線程來進行,這樣的話會出現嚴重的競爭條件:
注意:髒讀和volatile都有可能產生髒值。
測試的代碼在這裏。
結論
若是你已經在代碼裏使用到它了——我會感到很是高興。