
1. 同步
1.1. 代碼塊對一組變數的訪問看上去是串行的:每次只有一個執行緒可以訪問記憶體
-
1.1.1. 由synchronized關鍵字保護的代碼塊
-
1.1.2. 用java.util.concurrent.lock.Lock類的實體保護的代碼
-
1.1.3. java.util.concurrent包中的代碼
-
1.1.4. java.util.concurrent.atomic包中的代碼
-
1.1.4.1. 原子類不使用同步,至少在CPU編程方面是這樣
1.1.4.1.1. 使用CAS指令的執行緒在同時訪問同一資源時不會阻塞
-
1.1.4.2. 原子類利用了比較并交換(Compare and Swap,CAS)CPU指令
-
1.1.4.3. 包中的類使用了基于CAS的原語,而不是傳統的同步
-
1.2. 同步的目的是保護對記憶體中值(或變數)的訪問
1.3. 同步需要獨占訪問資源
- 1.3.1. 需要同步鎖的執行緒在另一個執行緒持有該資源時會阻塞
2. 同步的代價
2.1. 同步和可擴展性
-
2.1.1. 應用程式在同步塊中花費的時間會影回應用程式的可擴展性
-
2.1.2. 阿姆達爾定律(Amdahl's law)
-
2.1.2.1. 加速比=1÷((1-P)+P÷N)
-
2.1.2.2. P是并行運行的程式量
-
2.1.2.3. N是使用的執行緒數量
2.1.2.3.1. 假設每個執行緒都有可用的CPU
-
2.1.2.4. 隨著P減小,也就是說,隨著更多的代碼位于串行塊中,擁有多個執行緒的性能收益也會減少
-
2.2. 獲取同步鎖需要CPU周期
2.3. 獲取同步鎖的開銷
-
2.3.1. 如果鎖是無競爭的,即兩個執行緒沒有在同一時間試圖訪問鎖,那么這個開銷是非常小的
-
2.3.1.1. 非膨脹鎖(unin?ated lock)
2.3.1.1.1. 無競爭的synchronized鎖
2.3.1.1.2. 獲取一個非膨脹鎖的開銷在幾百納秒左右
-
2.3.1.2. 無競爭的CAS結構會有更小的性能損失
-
-
2.3.2. 有競爭的結構開銷會更大
-
2.3.2.1. 當第二個執行緒試圖訪問一個synchronized鎖時,可以預見鎖會變成膨脹的in?ated
-
2.3.2.2. 第二個執行緒必須等待第一個執行緒釋放鎖
-
2.3.2.3. 這個等待時間取決于應用程式
-
-
2.3.3. 在使用CAS指令的代碼中,競爭操作的開銷是不可預知的
-
2.3.3.1. 在最壞的情況下,兩個執行緒可能會陷入無限回圈
2.3.3.1.1. 因為每個執行緒修改CAS保護的值之后,發現另一個執行緒同時進行了修改
-
2.4. Java特有的,并取決于Java記憶體模型(Java Memory Model)
-
2.4.1. 不同于C++和C這樣的語言,它對關于同步的記憶體語意有嚴格的保證,并且該保證適用于基于CAS的保護、傳統的同步,以及volatile關鍵字
-
2.4.2. 變數會臨時存盤在暫存器中,這比直接在主記憶體中訪問它們要高效得多
-
2.4.3. 暫存器的值對其他執行緒來說是不可見的
-
2.4.4. 修改暫存器中值的執行緒必須在某個時刻將該暫存器重繪到主記憶體中,這樣其他執行緒才能看到這個值
-
2.4.5. 什么時候重繪暫存器的值,是由執行緒同步決定的
-
2.4.6. 對于標記為volatile的變數,無論什么時候被修改,都會被更新到主記憶體中
-
2.4.7. 將大量連續的、細粒度的呼叫包裝在一個同步塊中
- 2.4.7.1. 同步塊執行時間很長就不適用
2.5. 同步的記憶體語意、基于CAS的結構,以及volatile關鍵字會對性能產生負面影響,特別是在有很多暫存器的大型機器上
3. 避免同步
3.1. 避免同步物件的競爭是減輕其性能影響的有效方法
- 3.1.1. 在每個執行緒中使用不同的物件,這樣訪問物件時就不存在競爭了
3.2. 為了實作執行緒安全,很多Java物件是同步的,但它們未必需要共享
-
3.2.1. 通過使用執行緒區域變數,物件的總數受到了限制(使對GC的影響最小化),而且每個物件都不會有執行緒競爭
-
3.2.2. 執行緒區域變數永遠都不會發生競爭,它們非常適合保存實際上不需要在執行緒間共享的同步物件
3.3. 用基于CAS的替代方案
-
3.3.1. 在某種意義上,這并不能避免同步,而是以不同的方式解決問題
-
3.3.2. 在這種情況下,通過減少同步的損失,可以得到與避免同步相同的效果
-
3.3.3. 對于確實需要共享的物件,基于CAS的工具是一種避免傳統同步的方法
3.4. 如果對資源的訪問是無競爭的,基于CAS的保護會比傳統的同步稍微快一些
3.5. 如果訪問始終是無競爭的,完全無保護還會再快一些,并且可以避免邊界情況
3.6. 如果對資源的訪問存在輕度或者適度的競爭,基于CAS的保護會比傳統的同步更快(通常會快得多)
3.7. 隨著所訪問資源的競爭越來越激烈,傳統的同步將在某個時候成為更高效的選擇
- 3.7.1. 在實踐中,這種情況只發生在運行了很多執行緒的大型機器上
3.8. 當只讀取值而不寫入的時候,基于CAS的保護不會受競爭的影響
3.9. 沒有什么可以替代在代碼運行的實際生產條件下進行廣泛的測驗,只有這樣,才能確定某一特定方法的哪種實作更好
4. 偽共享
4.1. false sharing
4.2. 快取行共享(cache line sharing)
4.3. 對于頻繁修改volatile變數或退出同步塊的代碼,偽共享會顯著降低性能
4.4. 偽共享造成的最嚴重的損失,基本上每個寫操作都會使所有其他快取行失效,而且性能是串行的
4.5. 偽共享不一定涉及同步(或volatile)變數,每當CPU快取中的資料值被寫入時,持有相同資料范圍的其他快取必須失效
4.6. Java記憶體模型要求,只有在同步原語(包括CAS結構和volatile)結束時,資料才必須寫入主記憶體,所以這種情況是最常遇到的
4.7. 標準工具集中,沒有任何一個可以解決偽共享,因為這需要與處理器架構相關的專業知識
-
4.7.1. 某些原生分析器可以提供和給定代碼行每條指令的時鐘周期數(cycles per instruction,CPI)相關的資訊
-
4.7.2. 在一個回圈內,某個簡單指令的CPI很高,就表明代碼正在等待將目標記憶體重加載到CPU快取
4.8. 避免偽共享的主要方法是代碼檢查
4.9. 防止偽共享需要修改代碼
-
4.9.1. 理想的情況是,所涉及的變數可以不那么頻繁地寫入
- 4.9.1.1. 極少的寫入次數不太可能引起快取行的競爭,即使4個執行緒在回圈結束時同時更新結果,也不會對性能產生影響
-
4.9.2. 對變數進行填充,這樣它們就不會被加載到同一快取行上
- 4.9.2.1. 也可以使用填充來將沖突的變數移至不同的快取行
4.10. 避免偽共享最好的方式是將資料移至區域變數,稍后再存盤它們
5. @Contended注解
5.1. JDK私有類中的一個特性可以減少設定欄位上的快取競爭
5.2. 通過使用@sun.misc.Contended標記由JVM自動填充的變數來實作的
5.3. 這個注解是私有的
-
5.3.1. 在Java 8中,它屬于sun.misc包,沒有什么能阻止你在自己的代碼中使用這個包
-
5.3.2. 在Java 11中,它屬于jdk.internal.vm.annotation包,由于Java 11使用了模塊系統
- 5.3.2.1. 如果不用-add-exports標志將該包添加到java.base模塊匯出的類集中,就無法使用這個包編譯類
5.4. -XX:+RestrictContended標志
-
5.4.1. 默認是true
-
5.4.1.1. 意味著該注解僅限于JDK的類使用
-
5.4.1.2. JVM會忽略這個注解
-
-
5.4.2. 要讓應用程式代碼使用該注解,需要加上-XX:-RestrictContended標志
5.5. -XX:-EnableContended
-
5.5.1. 默認是true
- 5.5.1.1. -XX:+EnableContended
-
5.5.2. 禁用JDK的自動填充
- 5.5.2.1. 這會減小Thread和ConcurrentHashMap類的大小,這兩個類都使用這個注解來填充它們的實作,以防止偽共享
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/548196.html
標籤:其他
