《深入理解JVM》第13章現場安全與鎖優化

線程安全

當多個線程訪問一個對象時,如果可以不考慮線程的調度以及交替執行,也不需要額外的同步手段,或者調用方進行任何其他操作,所得到的結果都是正確,那麼就被稱爲線程安全
如果一個時間段只有一個線程在操作(寫操作之類)該對象,那必然也是線程安全的。或者說不影響對象狀態的多線程一起操作。
(就有點像買東西,大家都能看,但是如果要拆開包裝對它進行一些會影響到商品的一些性質之類的,只能「買」下來了)。

Java中的線程安全

線程安全可以當成一個強弱程度的概念而不用簡單當成是或否的概念。

不可變

不可變基本類型一定是線程安全的(不出現this溢出的情況下),因爲它無法修改。

絕對線程安全

基本上無法達到(也沒必要,代價高昂),過於嚴格,它的定義是不管運行環境如何都無需額外的同步措施,即可保證線程安全。就有點類似原子操作+原子操作!=原子操作 這個意思。

相對線程安全

通常意義下的線程安全,Java中的併發集合都是相對線程安全的。也就是保證對對象的單次操作下是線程安全的。

線程兼容

可通過調用端通過同步手段來同步。JDK中大多數API都是線程兼容的.

線程對立

無論採取什麼手段都無法達到線程安全。不過這種情況很少
比如Thread類中的suspend()和resume()(這倆方法現已廢棄),可能出現死鎖。

線程安全的實現方法

互斥同步

保證共享變量在同一時刻只能杯一條(或者一些)線程同時訪問。
有臨界區,互斥量和信號量這三種實現思路。

synchronized
synchronized關鍵字是一種很常用的手段,它經過javac編譯後,會被編譯爲monitorenter和monitorexit添加於修飾的代碼塊前後,如果是明確指明的鎖的是某對象,則以該對象作爲鎖,如果沒用,則根據是實例方法還是類方法來決定鎖實例對象還是類class對象。
具體可見本文對應部分
注意:

  • synchronized是可重入的。
  • synchronized是不可剝奪的。無條件的阻塞後面的線程。
  • 由於線程是映射到系統內核線程之上的,如果阻塞和喚醒由系統來完成,這會陷入用戶態到核心態的切換中,這會浪費一些時間,如果是同步代碼塊還比較簡短的情況下,相較而言就太浪費資源了。
  • 排他鎖
  • 非公平鎖:誰搶到歸誰,不考慮是否有等待更久的其他線程。

ReentrantLock
具體可見本文
比sync的區別:

  • 等待可中斷:長時間等待的線程可選擇放棄等待,改爲處理其他事情。
  • 公平鎖:根據等待的時間先後獲取鎖。
  • 鎖綁定多個條件:可有多個等待隊列。
  • 6以前ReentrantLock性能遠勝於sync。
  • 需要手動釋放鎖
非阻塞同步

基於衝突檢測的樂觀策略,不管風險,先操作,如果沒衝突,成功,如果有則再進行其他補償操作。
它的優勢在於少了 從用戶態到核心態的轉換、維護鎖計數器和檢查是否有被阻塞線程需要喚醒等操作的開銷。

各大系統中常用指令有:

  • 測試並設置(Test-and-Set)
  • 獲取並增加(Fetch-and-Increment)
  • 交換(Swap)
  • 比較並交換(CAS)
  • 加載鏈接/條件儲存(LL/SC)

Java中可使用CAS。
該指令需要三個操作數,內存位置V,預期值A,新值B,如果V當前值爲A則更新爲B。
在JDK5之後Java類庫中踩開始使用CAS操作,HotSpot虛擬機的內部對這些方法做了些特殊處理,即時編譯出來的結果是一條平臺相關處理器的CAS指令,沒有方法調用的過程,或者認爲是無條件內聯進去了。
JDK9之前只有Java類庫纔可使用CAS(限制了只有啓動類加載器加載的Class纔可訪問)。當然可以用過反射來使用它或者通過其他類庫來間接使用。
CAS簡單高效,不過它會嘗試很多「無用操作」,不斷的比較,而且存在ABA問題,當然這個JUC中已經有了帶版本號的CAS操作,所以也算問題不大。

無同步方案

如果沒有共享數據,自然就不需要同步。
1.可重入代碼, 被稱爲純代碼,指的是,執行的任何時候中斷它轉而去執行另一段代碼,返回時,原程序不會出現任何錯誤,結果也不會受到影響。比如僅基於參數的遞歸。
2.線程本地存儲, 將數據的可見性限制於本線程,共享數據完整代碼能否僅在一個線程中執行。如果可以,就可以使用本地存儲(比如生產者-消費者模式)。Java中也可使用ThreadLocal類來實現本地存儲功能。

鎖優化

自旋鎖與自適應自旋

由於阻塞線程會帶來用戶態到核心態的切換而導致資源的消耗,特別是在持有鎖的時間不算長時,資源的浪費就會比較明顯,JDK6之後默認開啓了自旋鎖就是爲了解決這個問題的,讓線程忙循環一陣,就能獲取鎖了,這就比阻塞效率高多了。
就相當於去一個店裏買東西,看貨架上沒有,如果倉庫裏有存貨,很快就能找出來,就不用明天再來看,還得來回跑一趟。
可是如果執行時間比較長,一直忙循環,那也是會消耗資源的,所以自旋一定次數後(默認爲10,也可通過-XX:PreBlockSpin來自行設置),就會停止自旋,轉換到阻塞狀態.
就相當於倉庫裏沒存貨,得去外地拉貨,總不可能在店裏等一天吧,這時候還是選擇回家,第二天再來比較好
不過JVM中除了根據默認值判斷,還有一套自適應的手段,如果一個鎖經常能夠僅僅通過自旋獲得,那麼就會嘗試多自旋一定的次數,如果靠自旋獲得比較少,則會少嘗試幾次。

鎖消除

在即時編譯期間,依據逃逸分析來判斷是否存在共享數據競爭的情況來判斷該鎖是否有必要。

鎖粗化

爲了減少用戶態核心態的轉換,可以適當擴大鎖的範圍。不過也別太大了,會減少併發度。

輕量級鎖

是一種自旋鎖,由於絕大多數鎖在整個同步週期都不存在競爭,所以這種效率還是很高的。
它的加鎖過程就是通過CAS嘗試把對象的對象頭更新爲指向鎖記錄(Lock Record)的指針,
如若成功,則表示該線程擁有了該鎖,Mark Word的鎖標誌位也會轉變爲00,
如果失敗,則意味着至少存在一條及以上的線程與之競爭。JVM就會首先檢查是否是當前線程擁有該鎖(Mark Word是否指向當前的線程棧幀)。否則說明被其他線程捷足先登了,這時就會自旋,如果超過一定的次數就會膨脹爲重量級鎖,如果競爭的線程爲兩條以上直接膨脹爲重量級鎖。
具體可見本文

偏向鎖

是一種特殊的無鎖狀態
就算沒有synchronized修飾,在默認情況下也會默認爲偏向鎖,偏向爲當前線程,直到調用了hashcode方法,覆蓋了偏向鎖中記錄的偏向線程的信息。
如果是synchronized中調用hashcode()方法,就會直接膨脹爲重量級鎖。
如果程序中的鎖,經常被大部分線程訪問,那麼偏向鎖就是多餘的,可使用-XX:UseBiasedLocking來關閉。
在這裏插入圖片描述
具體可見本文