1. 并發(fā)編程
1.1 并發(fā)編程的挑戰(zhàn)
并發(fā)編程的目的是為了加快程序的運(yùn)行速度, 但受限于上下文切換和死鎖等問題, 啟動(dòng)更多的線程并非能讓程序最大限度地并行執(zhí)行.
1.1.1 上下文切換
- CPU 通過分配時(shí)間片的方式來支持多線程.
- 通過時(shí)間片分配算法來循環(huán)執(zhí)行任務(wù), 任務(wù)切換前會(huì)保存上一個(gè)任務(wù)的狀態(tài), 以便以后再次運(yùn)行時(shí)的再加載.
- 時(shí)間片一般為幾十毫秒(ms), 所以感覺多個(gè)線程是同時(shí)運(yùn)行的.
- 上下文切換(Context Switch) : 任務(wù)從保存到再加載的過程.
- 并發(fā)執(zhí)行會(huì)產(chǎn)生線程創(chuàng)建和上下文切換的開銷, 所以并非一定比串行執(zhí)行更快.
- 減少上下文切換
- 無鎖并發(fā)編程. 多線程競(jìng)爭(zhēng)鎖時(shí), 會(huì)引起上下文切換.
- CAS 算法. 使用CAS 可以在無需加鎖的情況下, 進(jìn)行Atomic 原子操作.
- 使用最少線程, 避免創(chuàng)建不需要的線程.
- 協(xié)程. 在單線程中實(shí)現(xiàn)多任務(wù)的調(diào)度和切換.
1.1.2 死鎖
- 一旦產(chǎn)生死鎖, 就會(huì)造成系統(tǒng)功能不可用.
- 此時(shí), 業(yè)務(wù)是可感知的, 因?yàn)椴荒芾^續(xù)提供服務(wù)了.
- 通過dump 線程來查看到底是那個(gè)線程出現(xiàn)了問題.
- 避免死鎖的常用手段:
- 避免一個(gè)線程同時(shí)獲取多個(gè)鎖.
- 避免一個(gè)線程在持有鎖期間同時(shí)占用多個(gè)資源, 盡量保證每個(gè)鎖只占用一個(gè)資源.
- 嘗試使用定時(shí)鎖. 例如
lock.tryLock(timeout)
.
1.2 資源限制的挑戰(zhàn)
只有在串行執(zhí)行會(huì)浪費(fèi)資源時(shí), 將其修改為并行執(zhí)行才能加快運(yùn)行速度.
- 在并發(fā)編程時(shí), 程序的執(zhí)行速度受限于PC 硬件資源或軟件資源, 如網(wǎng)速, 硬盤讀寫速度, CPU 處理速度, 數(shù)據(jù)庫的鏈接數(shù)和socket 連接數(shù)等.
- 資源限制引發(fā)的問題:
- 在資源受限時(shí), 將串行執(zhí)行的代碼并發(fā)執(zhí)行, 其結(jié)果仍然是串行執(zhí)行. 由于增加了上下文切換和資源調(diào)度的消耗, 可能會(huì)使得程序的運(yùn)行速度更慢.
- 例如, 在較慢的網(wǎng)絡(luò)條件下, 下載一個(gè)大文件, 單線程比并發(fā)編程速度更快.
2. Java 內(nèi)存模型(JMM)
現(xiàn)代軟硬件的共同目標(biāo): 在不改變程序執(zhí)行結(jié)果的前提下, 盡可能提高并行度.
2.1 內(nèi)存模型的基礎(chǔ)
- 并發(fā)編程模型的兩個(gè)關(guān)鍵問題: 線程間如何通信, 以及線程之間如何同步.
- 通信: 交換信息的機(jī)制. 有兩種常見的方式:
- 共享內(nèi)存: 讀寫內(nèi)存中的公共狀態(tài)來進(jìn)行隱式通信.
- 消息傳遞: 無公共狀態(tài), 通過發(fā)送消息來顯示進(jìn)行通信.
- 同步: 用以控制不同線程間操作發(fā)生的相對(duì)順序的機(jī)制.
- 共享內(nèi)存的通信機(jī)制下, 必須進(jìn)行顯式的同步.
- 消息傳遞的通信機(jī)制下, 消息的發(fā)送順序隱式進(jìn)行了同步.
- JAVA 采用共享內(nèi)存模型, 線程間的通信過程對(duì)外完全透明.
- 通信: 交換信息的機(jī)制. 有兩種常見的方式:
2.2 JMM 的抽象結(jié)構(gòu)
- JAVA 中的內(nèi)存存儲(chǔ).
- 堆內(nèi)存: 存放實(shí)例域, 靜態(tài)域, 數(shù)組元素. 在線程間共享.
- 棧內(nèi)存: 存放局部變量, 方法定義參數(shù)和異常處理器參數(shù).
-
不會(huì)共享, 也不會(huì)有內(nèi)存可見性問題. 不受JMM 的影響.
-
- JMM 決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見.
- 定義了線程的本地內(nèi)存和主內(nèi)存之間的抽象關(guān)系.
- 主內(nèi)存負(fù)責(zé)存儲(chǔ)共享變量.
- 本地內(nèi)存涵蓋了緩存,寫緩存,寄存器及其他優(yōu)化. 它會(huì)存儲(chǔ)該線程讀寫共享變量的副本.
- 兩個(gè)線程間的通信過程, 必須經(jīng)過主內(nèi)存.
2.3 處理器和內(nèi)存的交互, 內(nèi)存屏障(Memory Barriers / Memory Fence)
- CPU 會(huì)使用寫緩存區(qū)來臨時(shí)保存需要向內(nèi)存寫入的數(shù)據(jù).
- 避免由于處理器停頓下來等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲, 從而保證了指令流水線持續(xù)運(yùn)行.
- 同時(shí), 通過以批處理的方式刷新寫緩存, 以及合并對(duì)同一內(nèi)存地址的多次寫, 減少了對(duì)內(nèi)存總線的占用.
-
但是, 每個(gè)CPU 上的寫緩存區(qū), 僅對(duì)該CPU 可見.
- 該特性會(huì)對(duì)內(nèi)存操作的執(zhí)行順序產(chǎn)生影響: CPU 對(duì)內(nèi)存的讀寫操作的執(zhí)行順序, 不一定與內(nèi)存實(shí)際發(fā)生的讀寫順序一致.
- 由此, CPU 允許對(duì)寫-讀操作進(jìn)行重排序(因?yàn)楸緛硪矡o法保證其順序性, 且重排序能夠提升性能).
- 在適當(dāng)?shù)奈恢貌迦雰?nèi)存屏障來禁止特定類型的CPU 重排序.
屏障類型 | 指令示例 |
---|---|
LoadLoad Barries | Load1; LoadLoad; Load2 |
StoreStore Barries | Store1; StoreStore; Store2 |
Load?Store Barries | Load1; LoadStore; Store2 |
StoreLoad Barries | Store1; StoreLoad; Load2 |
- 其中, StoreLoad 同時(shí)具有其它3個(gè)屏障的效果, 執(zhí)行它的開銷很大, 因?yàn)樗枰褜懢彺鎱^(qū)的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush).
2.4 happens-before
2.4.1 happens-before 的定義
- 闡述操作之間的內(nèi)存可見性:
- 如果一個(gè)操作的結(jié)果需要對(duì)另一個(gè)操作可見, 那么兩個(gè)操作之間必須要存在happends-before 關(guān)系.
- 兩個(gè)操作可以是一個(gè)線程內(nèi), 或者在不同的線程間.
- 存在happens-before 關(guān)系, 并不意味著Java 平臺(tái)的具體實(shí)現(xiàn)必須要按照關(guān)系指定的順序來執(zhí)行.
- 重排序后的執(zhí)行結(jié)果與按happens-before 關(guān)系執(zhí)行的結(jié)果一致時(shí), 該重排序是合法的.
- 如果一個(gè)操作的結(jié)果需要對(duì)另一個(gè)操作可見, 那么兩個(gè)操作之間必須要存在happends-before 關(guān)系.
- 目的: 在不改變程序結(jié)果的前提下, 盡可能提高程序執(zhí)行的并行度.
- as-if-serial: 保證單線程內(nèi)程序執(zhí)行結(jié)果不被改變, happens-before 保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變.
2.4.2 happens-before 規(guī)則:
- 程序順序規(guī)則: 一個(gè)線程中的每個(gè)操作, happens-before 于該線程中的任意后續(xù)操作.
- 監(jiān)視器鎖規(guī)則: 對(duì)一個(gè)鎖的解鎖, happens-before 于隨后對(duì)該鎖的加鎖.
- volatile 變量規(guī)則: 對(duì)一個(gè)volatile 域的寫, happens-before 于任意后續(xù)對(duì)這個(gè)volatile 域的讀.
- 傳遞性: A happens-before B, B happens-before C, 則A happens-before C.
- start() 規(guī)則: 線程A 執(zhí)行
ThreadB.start()
, 則該操作happens-before 于線程B 中的任意操作. - join() 規(guī)則: 如果線程A 執(zhí)行
ThreadB.join()
并成功返回, 則線程B 中的任意操作happens-before 于線程A 的ThreadB.join()
操作的成功返回.
2.4.3 程序順序規(guī)則
- 兩個(gè)具有happens-before 關(guān)系的操作, 僅僅要求前一個(gè)操作(的執(zhí)行結(jié)果)對(duì)后一個(gè)操作可見.
- 而不要求前一個(gè)操作要在后一個(gè)操作之前執(zhí)行.
- 如果A happens-before B, 但A 和B 之間不存在數(shù)據(jù)依賴性, 則可能會(huì)進(jìn)行重排序, 使得B 在A 之前執(zhí)行.
2.5 重排序
2.5.1 數(shù)據(jù)依賴性
- 如果兩個(gè)操作訪問同一變量, 且有一個(gè)是寫操作, 則這兩個(gè)操作存在數(shù)據(jù)依賴性.
- 它僅針對(duì)單個(gè)CPU 中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作.
2.5.2 as-if-serial 語義
- 無論如何重排序, 單線程程序的執(zhí)行結(jié)果不能被改變.
- 為了遵守該語義, 編譯器和CPU 不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序.
- 造成了一個(gè)幻覺: 單線程程序是按程序的順序來執(zhí)行的.
2.5.3 從源碼到指令序列的重排序
- 從JAVA 源代碼到最終實(shí)際執(zhí)行的指令序列, 會(huì)經(jīng)歷3種重排序:
- 編譯器優(yōu)化重排序: 在不改變單線程程序語義的前提下, 重新安排語句的執(zhí)行順序.
- 指令級(jí)并行的重排序: 如果不存在數(shù)據(jù)依賴性, CPU 可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序.
- 采用ILP(指令級(jí)并行技術(shù)) 來將多條指令重疊執(zhí)行.
- 內(nèi)存系統(tǒng)的重排序: 由于CPU 私用緩存和讀/寫緩沖區(qū), 加載和存儲(chǔ)操作看起來是在亂序執(zhí)行.
- 1 屬于編譯器重排序, 2和3 屬于處理器重排序.
2.5.4 JMM 的設(shè)計(jì)初衷
- 程序員希望內(nèi)存模型易于理解和編程, 所以需要一個(gè)強(qiáng)內(nèi)存模型.
- 編譯器和CPU 則希望內(nèi)存模型對(duì)其有最小的束縛, 方便做優(yōu)化來提高性能. 所以需要一個(gè)弱內(nèi)存模型.
- 重排序會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題.
- JMM 的編譯器重排序規(guī)則會(huì)禁止特定類型的重排序.
- JMM 的處理器重排序規(guī)則會(huì)在生成指令序列時(shí), 插入特定類型的內(nèi)存屏障來禁止特定類型的重排序.
- JMM 屬于語言級(jí)的內(nèi)存模型. 它確保在不同的編譯器和處理器平臺(tái)上, 通過禁止特定類型的編譯器和處理器重排序, 對(duì)外提供一致的內(nèi)存可見性保證.
2.5.5 JMM 對(duì)待重排序的策略
- 對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序, JMM 要求編譯器和CPU 必須禁止這種重排序.
- 對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序, JMM 不做任何要求, 即允許這種重排序.
- 例如, 若認(rèn)定鎖只會(huì)被單線程訪問, 則消除之; 若volatile 只會(huì)被單線程訪問, 則已普通變量對(duì)待之.
2.5.6 控制依賴關(guān)系
- 前序操作是條件語句(if, while...), 則后續(xù)操作和前序之間就產(chǎn)生了控制依賴關(guān)系.
- 當(dāng)代碼中存在控制依賴性時(shí), 會(huì)影響指令序列執(zhí)行的并行度.
- 采用猜測(cè)(Speculation) 執(zhí)行來克服控制相關(guān)性對(duì)并行度的影響.
- 例如, CPU 會(huì)執(zhí)行后續(xù)操作,并將其計(jì)算結(jié)果保存到重排序緩存(Reorder Buffer: ROB) 的硬件緩存中,如果條件為真, 直接使用該結(jié)果順序執(zhí)行.
-
在多線程中, 對(duì)存在控制依賴的操作重排序, 可能會(huì)改變程序的執(zhí)行結(jié)果.
2.6 順序一致性
2.6.1 數(shù)據(jù)競(jìng)爭(zhēng)與順序一致性
- 數(shù)據(jù)競(jìng)爭(zhēng):
- 兩個(gè)線程對(duì)同一個(gè)變量, 分別進(jìn)行讀和寫, 且沒有通過同步對(duì)讀寫進(jìn)行排序.
- 包含數(shù)據(jù)競(jìng)爭(zhēng)的代碼, 執(zhí)行結(jié)果不定.
- 順序一致性:
- 正確同步的程序, 其執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果.
- 同步指的是對(duì)同步原語(synchronized, volatile和final)的使用.
2.6.2 順序一致性內(nèi)存模型
未同步程序在JMM 中的執(zhí)行, 整體是無序的, 其執(zhí)行結(jié)果無法預(yù)知. 而在順序一致性模型中, 所有線程看到的是一個(gè)一致的整體執(zhí)行順序.
- 對(duì)外提供了極強(qiáng)的內(nèi)存可見性保證.
- 一個(gè)線程中的所有操作必須按照程序的順序執(zhí)行.
- 不管程序是否同步, 所有線程都只能看到一個(gè)單一的操作執(zhí)行順序. 每個(gè)操作都必須原子執(zhí)行并立刻對(duì)所有線程可見.
- 一個(gè)單一的全局內(nèi)存, 通過左右搖擺的開關(guān)連接到任意一個(gè)(僅一個(gè))線程.
- 例如, 兩個(gè)線程A 和B, 分別有操作A1, A2, A3, B1, B2, B3.
- 在使用監(jiān)視器鎖來正確同步(A 先B 后)時(shí), 執(zhí)行順序?yàn)? A1 -> A2 -> A3 -> B1 -> B2 -> B3.
- 在未同步時(shí), 可能的執(zhí)行順序是: A1 -> B1 -> B2 -> A2 -> A3 ->B3.
- 未同步的多線程程序, 在順序一致性模型中雖然整體執(zhí)行順序是無序的, 但所有線程都只能看到一個(gè)一致的整體執(zhí)行順序(因?yàn)槊總€(gè)操作立即對(duì)任意線程可見).
- 如果A 看到的是: A1 -> B1 -> B2 -> A2 -> A3 ->B3, 那么B 看到的也一定是.
- 而未同步程序在JMM 中, 不但整體的執(zhí)行順序是無序的, 且所有線程看到的操作執(zhí)行順序也可能不一致(本地內(nèi)存不會(huì)及時(shí)的刷新到主內(nèi)存中).
2.6.3 未同步程序的執(zhí)行特性
- 對(duì)于未同步或未正確同步的多線程程序, JMM 只提供最小安全性:
- 線程執(zhí)行時(shí)讀取到的值, 要么是之前某個(gè)線程寫入的值, 要么是默認(rèn)值(0, false, null).
- JMM 保證線程讀操作讀取到的值不會(huì)無中生有(Out of Thin Air).
- JVM 在堆上分配對(duì)象時(shí), 首先會(huì)對(duì)內(nèi)存空間進(jìn)行清零, 然后在其上分配對(duì)象(內(nèi)部會(huì)同步這兩個(gè)操作).
- 在已清零的內(nèi)存空間(Pre-zeroed Memory)分配對(duì)象時(shí), 域的默認(rèn)初始化已經(jīng)完成了.
- JMM 不保證對(duì)64 位的long/double 型變量的寫操作具有原子性.
- CPU 和內(nèi)存間的數(shù)據(jù)傳遞, 通過總線事務(wù)來確保所有CPU 對(duì)內(nèi)存的訪問以串行化的方式進(jìn)行.
- 任意時(shí)刻, 最多只能有一個(gè)CPU 可以訪問內(nèi)存, 確保了單個(gè)總線事務(wù)之中的內(nèi)存讀寫操作具有原子性.
- 在一些32 位CPU 上, 保證64 位數(shù)據(jù)寫操作的原子性, 會(huì)產(chǎn)生較大的開銷.
- JMM 可能會(huì)將一個(gè)64 位寫操作分拆為兩個(gè)32 位的寫, 從而被分配到不同的總線事務(wù)上, 不再具有原子性.
- JDK 1.5 以后, 保證了讀的原子性, 而寫允許被分拆.