四、高效并發(fā)
1. Java內(nèi)存模型與線程
1.1 概述
計算機大部分時間都花磁盤I/O,網(wǎng)絡通訊,數(shù)據(jù)庫訪問,CPU大部分時間都在等待其他資源的狀態(tài),因此需要同時處理多個任務
另一個并發(fā)應用場景,就是服務端同時對多個客戶端提供服務
1.2 硬件效率與一致性
CPU運算速度比訪問內(nèi)存速度快得多,因此加上了高速緩存,將數(shù)據(jù)復制到緩存中,CPU從緩存中讀取數(shù)據(jù)高速運算,運算結束后把結果從緩存同步到主內(nèi)存。這樣可以解決處理器和內(nèi)存速度矛盾,但是會引入緩存一致性問題。多處理器訪問同一主內(nèi)存區(qū)域,各自緩存可能不一致。因此各處理器訪問內(nèi)存需要遵循協(xié)議:MSI MESI MOSI Synapse Firefly Dragon Protocol等
處理器對代碼可能進行亂序執(zhí)行優(yōu)化。相應的,虛擬機即時編譯器也有類似的指令重排序優(yōu)化
1.3 Java內(nèi)存模型
主內(nèi)存與工作內(nèi)存
所有共享的變量在主內(nèi)存中,每個線程有自己的工作內(nèi)存,線程對變量的操作都必須在工作內(nèi)存中進行,而不能直接操作主內(nèi)存,線程間變量傳遞需要主內(nèi)存
內(nèi)存間交互操作
八個指令:
lock:作用于主內(nèi)存,把主內(nèi)存變量標識為線程獨占
unlock:作用于主內(nèi)存,把主內(nèi)存變量解鎖,解鎖后其他線程才能鎖定
read:作用于主內(nèi)存,把變量的值從主內(nèi)存?zhèn)鞯焦ぷ鲀?nèi)存
load:作用于工作內(nèi)存,把主內(nèi)存得到的值,放到工作內(nèi)存變量副本
use:作用于工作內(nèi)存,虛擬機遇到需要使用變量的字節(jié)碼指令時會執(zhí)行。把變量的值傳給執(zhí)行引擎
assign:作用于工作內(nèi)存,虛擬機遇到給變量賦值的字節(jié)碼指令時會執(zhí)行。把從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量
store:作用于工作內(nèi)存,把工作內(nèi)存中的變量的值傳到主內(nèi)存
write:作用于主內(nèi)存,把從工作變量中得到的值放到主內(nèi)存變量中
注意:
read、load與store、write必須按先后順序執(zhí)行,但是中間可以穿插其他操作
read、load與store、write必須成對出現(xiàn),即不允許從一邊讀了但另一邊不接受
assign了就一定要同步回主內(nèi)存
沒有assign過不允許同步回主內(nèi)存
不允許工作內(nèi)存中直接使用未初始化(assign/load)的變量,use store操作前,必須執(zhí)行過了assign和load
lock可以執(zhí)行多次,但需要解鎖同樣次數(shù)
執(zhí)行l(wèi)ock前,會清空工作內(nèi)存中此變量的值。執(zhí)行引擎使用此變量前,需要先assign或load重新初始化
沒有l(wèi)ock不允許unlock
unlock前必須把變量同步回主內(nèi)存,即執(zhí)行store、write
對于volatile型變量規(guī)則
volatile可以保證變量對所有線程可見,但并不是絕對線程安全,多寫場景下仍然有并發(fā)問題,因為寫的操作不是原子的。volatile適合一寫多讀場景
volatile另一個語義是禁止指令重排序優(yōu)化
volatile的讀效率與正常變量差不多,寫效率慢一寫,因為需要插入內(nèi)存屏障
對于long和double型變量規(guī)則
雖然虛擬機規(guī)范中允許把64位數(shù)據(jù)分為兩次32位操作,但具體實現(xiàn)時,仍然會把64位數(shù)據(jù)作為原子操作
原子性、可見性、有序性
原子性:6個基本操作是原子性的,如果不能滿足需要,可以用lock、unlock指令,對應字節(jié)碼指令monitorenter、monitorexit
可見性:一個線程修改了變量的值,其他線程能夠立即知道修改的值。volatile、final、synchronized三個關鍵字都能夠保證變量可見性。被final修飾的字段在構造器中一旦被初始化完成,并且構造器沒有把this的引用傳遞出去,那么其他線程就能看見final字段的值。synchronized的可見性指的是,unlock操作前,必須把變量從工作內(nèi)存同步到主內(nèi)存
有序性:volatile可以禁止指令重排序,synchronized是可以保證同一時刻只有一個線程訪問
先行發(fā)生原則
天然有先后順序,無需進行同步控制
1.4 Java與線程
一對一,映射到輕量級進程
調(diào)度方法:搶占式調(diào)度
線程狀態(tài)轉(zhuǎn)換
2. 線程安全與鎖優(yōu)化
2.1 線程安全
多個線程訪問一個對象,不用考慮線程調(diào)度和交替執(zhí)行,不用額外同步,調(diào)用這個對象都可以獲取到正確的結果
Java中的線程安全
不可變:如final修飾,比如String、Number的部分子類(Integer、Long等),注意,AtomicInteger等不屬于
絕對線程安全:沒有
相對線程安全:線程安全的容器,比如Vector、CurrentHashMap,Collections中的SynchronizedCollection()方法包裝的集合
線程兼容:調(diào)用端進行同步,可以保證多線程安全使用,比如HashMap
線程對立:很少,已廢棄
實現(xiàn)方法
互斥同步:即加鎖,synchronized、Lock
非阻塞同步:無鎖,CAS
無同步方案:線程本地存儲,如ThreadLocal
2.2 鎖優(yōu)化
自旋鎖與自適應自旋
忙循環(huán),CPU不讓出執(zhí)行權
自旋一定次數(shù)還沒有獲取到鎖,可以省略自旋
鎖消除
基于逃逸分析,如果不會被其他線程訪問到,可以消除同步措施
鎖粗化
循環(huán)加鎖擴展到外部只加一次鎖
輕量級鎖
對象頭包含兩部分信息:1.對象自身運行時數(shù)據(jù),比如哈希碼,分代年齡,64位OS中長度是64bit;2.方法區(qū)類型數(shù)據(jù)指針,如果是數(shù)組,則還有數(shù)組長度
[圖片上傳失敗...(image-79696f-1651548783059)]
線程中的LockRecord空間,可以存儲對象markword的拷貝,即Displaced Mark Word。加鎖的時候,CAS地更新對象markword為指向LockRecord的指針,如果更新成功,則標志位置為了00,加鎖成功,沒有則加鎖失敗。解鎖過程就是把指針更新回Displaced Mark Word,如果更新成功則解鎖成功,失敗則說明有其他線程嘗試獲取過鎖,要釋放鎖的時候,喚醒被掛起的線程。如果有2個以上線程競爭,則升級為重量級鎖,標志位為10.
偏向鎖
對一個無鎖對象,把線程id記錄到markword中,再有一個線程來獲取鎖,標志位恢復01(無鎖)或00(輕量級鎖)