Java 并發程式設計 第一部分
- 全文内容
-
- 并發
-
- 是什麼?
-
- 同步與異步
- 并發與并行
- 程序、線程、協程
-
- 協程
- Java中的并發關鍵字
-
- synchronized
-
- 表現形式
- Java對象頭
- volatile
-
- CPU的術語定義
- 使用優化
- 原子操作
-
- 處理器如何實作原子操作
- Java如何實作
- 為什麼?
- 挑戰
-
- 上下文切換
- 死鎖
- 資源限制
- 多線程
-
- JMM
-
- 主記憶體與工作記憶體
- 記憶體間互動操作
- 重排序
-
- 從源代碼到指令序列的重排序
- as-if-serial語義
- JMM解決方案
-
- 記憶體屏障
- happens-before
-
- 為什麼?
-
- JMM的設計示意圖
- 是什麼?
-
- volatile規則
- start()規則
- join()規則
- volatile的記憶體語義
-
- 是什麼?
- 如何做到?
-
- volatile重排序規則表
- 基于保守政策的JMM記憶體屏障插入政策
- 指令序列示意圖
- 鎖的記憶體語義
-
- 鎖的釋放-擷取建立的happens-before關系
- 鎖記憶體語義的實作
- final域的記憶體語義
- 雙重檢查鎖定與延遲初始化
-
- 雙重檢查鎖定的由來
- 問題的根源
- 基于volatile的解決方案
- 基于類初始化的解決方案
- 解決方案的比較
- Java中的線程
-
- 線程的狀态
-
- 有哪些?
- 變遷
- Daemon線程
- 優先級
- 啟動和終止線程
-
- 建立
- 中斷
- 線程間的通信
-
- synchronized
-
- 底層實作
- JVM的優化
- 等待/通知機制
- Thread.join()的使用
- ThreadLocal的使用
- 鎖
-
- Lock接口
-
- 我有synchronized沒有的
- API
- 隊列同步器(AQS)
-
- AbstractQueuedSynchronizer
- 實作分析
-
- 同步隊列(FIFO)
- 獨占式同步狀态擷取與釋放
- 共享式同步狀态擷取與釋放
- 獨占式逾時擷取同步狀态
- 重入鎖(ReentrantLock)
- 讀寫鎖(ReentrantReadWriteLock)
- LockSupport工具
- Condition接口
-
- API
- 實作分析
- 引用
你好! 這是我對于Java 并發程式設計相關知識點的梳理與思考,希望對你能有所幫助;菜鳥萌新,問題多多,歡迎指出,謝謝!本文章以圖為主,如果文章中沒有圖檔,麻煩移步: 連結.
全文内容
并發
是什麼?
同步與異步
并發與并行
程序、線程、協程
- 程序作為擁有資源的基本機關,線程作為排程和配置設定的基本機關。
- 資源配置設定給程序,同一程序的所有線程共享該程序的所有資源。
- 多程序的程式要比多線程的程式健壯(程序崩潰不影響其它程序),但在程序切換時,耗費資源較大,效率要差一些
- 程序/線程之間的親緣性決定了同一個程序/線程隻在某個cpu上運作,避免因切換帶來的CPU的L1/L2 cache失效而造成性能損失
協程
- 不被作業系統核心所管理,而完全是由程式所控制(也就是在使用者态執行)
- 極高的執行效率:因為子程式切換不是線程切換,而是由程式自身控制,是以,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯;
Java中的并發關鍵字
synchronized
- 由于Java中的線程是與作業系統的原生線程一一對應的,是以當阻塞一個線程時,需要從使用者态切換到核心态執行阻塞操作,這是很耗時的操作,而synchronized的使用就會導緻上下文切換
表現形式
- 對于普通同步方法,鎖是目前執行個體對象。
- 對于靜态同步方法,鎖是目前類的Class對象。
- 對于同步方法塊,鎖是Synchonized括号裡配置的對象。
Java對象頭
volatile
CPU的術語定義
- 為了提高處理速度,處理器不直接和記憶體進行通信
- Lock字首指令會引起處理器緩存回寫到記憶體(鎖緩存/鎖總線)
- 一個處理器的緩存回寫到記憶體會導緻其他處理器的緩存無效(緩存一緻性協定)
使用優化
- 追加位元組
原子操作
處理器如何實作原子操作
Java如何實作
為什麼?
- 對應用系統性能和吞吐量要求
挑戰
上下文切換
- 在單CPU時代多線程程式設計是沒有太大意義的,并且線程間頻繁的上下文切換還會帶來額外開銷
死鎖
- 互斥條件
- 請求并持有條件
- 不可剝奪條件
- 環路等待條件
資源限制
多線程
JMM
主記憶體與工作記憶體
- CPU、記憶體和 I/O 裝置 存在速度差異性問題
記憶體間互動操作
重排序
從源代碼到指令序列的重排序
as-if-serial語義
JMM解決方案
- 對于處理器重排序,插入記憶體屏障指令
記憶體屏障
happens-before
如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens- before關系
為什麼?
JMM的設計示意圖
是什麼?
volatile規則
start()規則
join()規則
volatile的記憶體語義
是什麼?
如何做到?
volatile重排序規則表
基于保守政策的JMM記憶體屏障插入政策
指令序列示意圖
volatile寫
volatile讀
- 在實際執行時,隻要不改變volatile寫-讀的記憶體語義,編譯器可以根據具體情況省略不必要的屏障
-
JSR-133為什麼要增強volatile的記憶體語義?
嚴格限制編譯器和處理器對volatile變量與普通變量的重排序,確定volatile的寫-讀和鎖的釋放-擷取具有相同的記憶體語義。
鎖的記憶體語義
鎖的釋放-擷取建立的happens-before關系
鎖記憶體語義的實作
concurrent包的實作
final域的記憶體語義
雙重檢查鎖定與延遲初始化
雙重檢查鎖定的由來
問題的根源
基于volatile的解決方案
基于類初始化的解決方案
解決方案的比較
Java中的線程
線程的狀态
有哪些?
變遷
Daemon線程
優先級
- 程式正确性不能依賴線程的優先級高低
啟動和終止線程
建立
- 實作Runnable接口的run方法
- 繼承Thread類并重寫run的方法
- 使用FutureTask方式,實作Callable接口的call方法
中斷
- boolean isInterrupted():檢測目前線程是否被中斷,如果是傳回true,否則傳回false
- boolean interrupted():檢測目前線程是否被中斷,如果是傳回true,否則傳回false。與isInterrupted不同的是,該方法如果發現目前線程被中斷,則會清除中斷标志,并且該方法是static方法,可以通過Thread類直接調用。另外從下面的代碼可以知道,在interrupted()内部是擷取目前調用線程的中斷标志而不是調用interrupted()方法的執行個體對象的中斷标志。
- 從Java的API中可以看到,許多聲明抛出InterruptedException的方法(例如Thread. sleep(long millis)方法)這些方法在抛出InterruptedException之前,Java虛拟機會先将該線程的中斷辨別位清除,然後抛出InterruptedException,此時調用isInterrupted()方法将會傳回false。
線程間的通信
synchronized
底層實作
在Java虛拟機(HotSpot)中,monitor是由ObjectMonitor實作的(位于HotSpot虛拟機源碼ObjectMonitor.hpp檔案,C++實作的)
ObjectMonitor中有幾個關鍵屬性:
- _owner:指向持有ObjectMonitor對象的線程
- _WaitSet:存放處于wait狀态的線程隊列
- _EntryList:存放處于等待鎖block狀态的線程隊列
- _recursions:鎖的重入次數
-
_count:用來記錄該線程擷取鎖的次數
ObjectMonitor中有幾個關鍵方法:
- enter(TRAPS)
- exit(TRAPS)
- wait(jlong millis, bool interruptable, TRAPS)
- nofity(TRAPS)
JVM的優化
- 鎖更新
-
鎖消除
JIT編譯器借助逃逸分析(Escape Analysis)技術來判斷同步塊所使用的鎖對象是否隻能夠被一個線程通路,如果被證明,就會取消對這部分代碼的同步。
-
鎖粗化
盡量減小鎖的粒度,可以避免不必要的阻塞。但是如果在一段代碼中連續的用同一個螢幕鎖反複的加鎖解鎖,甚至加鎖操作出現在循環體中的時候,就會導緻不必要的性能損耗,這種情況就需要鎖粗化。
等待/通知機制
- 使用wait()、notify()和notifyAll()時需要先對調用對象加鎖
- 調用wait()方法後,線程狀态由RUNNING變為WAITING,并将目前線程放置到對象的等待隊列
- notify()或notifyAll()方法調用後,等待線程依舊不會從wait()傳回,需要調用notify()或notifAll()的線程釋放鎖之後,等待線程才有機會從wait()傳回
- notify()方法将等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是将等待隊列中所有的線程全部移到同步隊列,被移動的線程狀态由WAITING變為BLOCKED
- 從wait()方法傳回的前提是獲得了調用對象的鎖
Thread.join()的使用
- 線程A執行了thread.join()語句
- 目前線程A等待thread線程終止之後才從thread.join()傳回
ThreadLocal的使用
鎖
Lock接口
我有synchronized沒有的
特性 | API |
---|---|
能響應中斷 | lockInterruptbly() |
非阻塞式的擷取鎖 | tryLock() |
支援逾時 | tryLock(long time, timeUnit) |
可實作公平鎖 | ReentrantLock(ture) |
可以綁定多個條件 | newCondition() |
API
隊列同步器(AQS)
基于模闆方法模式
AbstractQueuedSynchronizer
通路或修改同步狀态
可重寫的方法
模闆方法
實作分析
同步隊列(FIFO)
節點的屬性類型與名稱以及描述
隻有前驅節點是頭節點才能夠嘗試擷取同步狀态的原因:
- 頭節點是成功擷取到同步狀态的節點,而頭節點的線程釋放了同步狀态之後,将會喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭節點。
- 維護同步隊列的FIFO原則
獨占式同步狀态擷取與釋放
節點自旋擷取同步狀态
解釋:由于非首節點線程前驅節點出隊或者被中斷而從等待狀态傳回,随後檢查自己的前驅是否是頭節點,如果是則嘗試擷取同步狀态。
獨占式同步狀态擷取流程,也就是acquire(int arg)方法調用流程:
同步狀态釋放
共享式同步狀态擷取與釋放
獨占式逾時擷取同步狀态
重入鎖(ReentrantLock)
讀寫鎖(ReentrantReadWriteLock)
ReentrantReadWriteLock展示内部工作狀态的方法
LockSupport工具
LockSupport提供的阻塞和喚醒方法:
Condition接口
API
實作分析
同步隊列與等待隊列
目前線程加入等待隊列:
節點從等待隊列移動到同步隊列
引用
[1]: 連結 https://mp.weixin.qq.com/s/-PRq4ChaCkEFB_DJyyKhvg
[2]: 《Java并發程式設計的藝術》 方騰飛 魏鵬 程曉明
[3]: 《Java并發程式設計之美》翟陸續,薛賓田