java線程安全的實現方法總結學習

java語言中的線程安全
  1. 將安全程度由強到弱排序:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立,共五種情況
  2. 不可變:典型的就是String類,這個就不多贅言。還有枚舉類,以及Number類的部分子類,如Long/Double/BigInteger等大數類型。
  3. 絕對線程安全:不論運行時環境如何,調用者都不用採取額外的同步措施。
  4. 相對線程安全:這是大家平常所提到的“線程安全”的級別,如集合類中的Vector/HashTable這類。
  5. 線程兼容:本身不是線程安全的,當時可以在調用時採取一些手段保證安全,比如典型的HashMap
  6. 線程對立:不論人如何採用手段都不能保證線程安全。典型:已經廢棄的Thread.suspend/resume方法。這兩個一旦同時調用就會有死鎖的風險,因爲它們的操作是對立的。
線程安全的實現方法
  1. 最基本的:synchronized關鍵字。這個方法是最常用的,它通過互斥的方式保證同步。我們知道java中有幾個操作是可以保證原子性的,其中lock/unlock就是一對。雖然java沒有提供這兩個字節碼的接口,但是我們可以通過monitorenter/monitorexit,而synchronized會在塊的前後調用兩個字節碼指令。同時synchronize對於同一條線程來說是可重入的;其次它也是阻塞的。我們知道java線程是映射到操作系統上的,而且是混用的內核態線程和用戶態線程(N:M),而將線程從阻塞/喚醒,需要將線程從用戶態轉換到內核態,這樣會消耗太亮的資源,所以synchronize是一個重量級鎖
  2. 另外一種和synchronize類似的方法:ReentrantLock。它們兩個的區別:(1)synchronize是隱式的,只要塊內的代碼執行完,就會釋放當前的鎖;而後者需要顯式的調用unlock()方法手動釋放,所以經常搭配try/finally方法(忘記在finally中unlock是非常危險的) (2)後者可以選擇等待中斷——即在當前持有鎖線程長期不釋放鎖的情況下,正在等待的線程可以選擇放棄等待選擇處理其他的事情。 (3) 後者可以選擇公平鎖(雖然默認是非公平的,因爲公平鎖的吞吐量很受影響)即先來後到,按申請的順序獲得鎖。 (4)可以綁定多個條件
  3. 前面提到的兩種方式都是通過互斥來達到同步的目的,這其實是悲觀鎖的一種。下面介紹的是樂觀鎖,基於衝突檢測的併發策略,不需要將線程掛起,因此又被成爲非阻塞同步。
  4. 典型:CAS(Compare And Swap),通過Unsafe類提供。有三個操作數,內存位置、舊的預期值、和新的值;當且僅當內存地址V符合預期值A時,執行將值更新爲新的預期值B。  存在的問題:“ABA”情況,即原值爲A,但在檢測之前發生了改變,變成了B,同時也在檢測時變回了A;即不能保證這個值沒有被其他線程更改過。
  5. 接下來是無同步方案:
  6. 可重入代碼(純代碼):是一種無同步方案,在執行的任何時候去中斷,轉而執行其他的代碼;在重新返回代碼後,不會出現任何的錯誤。可重入性->線程安全,充分不必要條件。即可重入性的代碼都是線程安全的,但反之不一定。簡單判斷原則:一個方法的返回結果是可預測的,只要輸入了相同的數據,就都能返回相同的結果。
  7. 線程本地存儲:即利用ThreadLocal類;每個Thread類中都有一個變量ThreadLocalMap,默認是爲null的。它將爲每一個線程創立一個該變量的副本。這樣線程之間就不存在數據徵用的問題了。適用情況:(1)數據庫的Connection連接 (2)WEB中的“一個請求對應一個服務器線程”,在知乎上看到一個回答,解釋的蠻清晰的。(3)Spring中創建的默認模式是Singleton單例 (4)“生產者-消費者問題” 

    ThreadLocal就是變量在不同線程上的副本,不同線程不共享,所以對變量改動時就不需要考慮線程間同步的問題了

    ThreadLocal在web應用開發中是一種很常見的技巧,當web端採用無狀態寫法時(比如stateless session bean和spring默認的singleton),就可以考慮把一些變量放在ThreadLocal中

    舉個簡單例子,以理解意思爲主:你有兩個方法A和B都要用到變量userId,又不想傳來傳去,一個很自然的想法就是把userId設爲成員變量,但是在無狀態時,這樣做就很可能有問題,因爲多個request在同時使用同一個instance,userId在不同request下值是不一樣的,就會出現邏輯錯誤
    但由於同一個request下一般都是處於同一個線程,如果放在ThreadLocal的話,這個變量就被各個方法共享了,而又不影響其他request,這種情況下,你可以簡單把它理解爲是一種沒有副作用的成員變量


    作者:卡斯帕爾
    鏈接:https://www.zhihu.com/question/21709953/answer/19066232
    來源:知乎
    著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
  8. Volidate+原子類:我們知道volidate是可以滿足可見性和禁止指令重排序,不能保證同步是因爲很多時候,比如++自加操作不是原子性的。
鎖優化

自旋鎖:
  1. 解決問題:在互斥同步中,最爲影響性能的操作是阻塞的實現;而同時大多數的共享數據的鎖定只持續很短的時間,因此而阻塞不值得。因此設立了自旋鎖。
  2. 出現時間:JDK 1.4.2 於JDK 1.6中默認開啓 並調整爲自適應的自旋鎖(以前是默認10次)
  3. 定義:假設物理機器擁有多個處理器(現在基本都有,多核CPU),能夠並行處理兩個及以上的線程,那麼可以讓後面的那個線程“稍微等一下”,即佔用CPU資源但不進行處理,觀察持有鎖的線程是否很快就會放棄鎖。而爲了達到等待的效果,讓線程執行一個忙循環(即自旋)
  4. 優缺點:對於前面很快釋放鎖的線程來說,性能提高很明顯;而如果前面的一直不釋放,就很尷尬(佔着茅坑不拉屎)。
鎖消除
  1. 定義:在代碼上要求同步,但被檢測到不可能存在共享數據競爭的鎖進行消除
  2. 適用:如果堆上的數據都不會逃逸出去,從而無法被其他線程訪問到,那麼實際上它是線程隔離的,這時候可以把它當做棧上的數據對待。
  3. 例子:String類的string s=s1+s2;在JDK 1.5之後實際上會被優化成StringBuffer類對象兩次append()操作。我們知道StringBuffer是線程安全的,append()操作是synchronize同步了的。而實際上這個對象並不會逃逸出方法,自然可以消除掉。
鎖粗化
  1. 這個概念比較相對於上面的鎖消除,我們在一般情況下推薦將同步塊寫的儘可能小,使得需要同步 的操作數量儘可能小。但在對一個對象反覆加鎖和解除的情況下,也會導致不必要的操作損耗。
  2. 例子:還是上面那個例子,幾次append其實都是針對StringBuffer的對象實例,那麼可以將其同步的範圍擴展到第一次append----最後一次append之間,這樣只需要加一次鎖就可以了。即鎖的粗化。
輕量級鎖和偏向鎖
  1. 適用:在同步對象無競爭的情況下(只有一個線程需要同步對象)
  2. 採取方法:輕量級鎖採取CAS操作替代synchronize(在無競爭情況下),有競爭則會膨脹回重量級鎖;偏向鎖直接就不要同步操作了,CAS也不要了,每次這個線程進入同步塊都不再進行任何同步操作,直到另外一個對象嘗試去獲取這個對象(競爭)
  3. 啓用版本:JDK 1.6之後 這兩個鎖都默認開啓
  4. 侷限:在競爭情況下,比重量級鎖更慢。注意,這並不是來替代重量級的,而是優化。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章