一文看懂Java鎖機(jī)制
點(diǎn)擊上方“碼農(nóng)突圍”,馬上關(guān)注 這里是碼農(nóng)充電第一站,回復(fù)“666”,獲取一份專屬大禮包 真愛,請(qǐng)?jiān)O(shè)置“星標(biāo)”或點(diǎn)個(gè)“在看”

背景知識(shí)
指令流水線

cpu多級(jí)緩存

問題引入
原子性
讀取變量i的值
進(jìn)行加一操作
將新的值賦值給變量i
read 從主存讀取到工作內(nèi)存 (非必須)
load 賦值給工作內(nèi)存的變量副本(非必須)
use 工作內(nèi)存變量的值傳給執(zhí)行引擎
執(zhí)行引擎執(zhí)行加一操作
assign 把從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量
store 把工作內(nèi)存中的一個(gè)變量的值傳遞給主內(nèi)存(非必須)
write 把工作內(nèi)存中變量的值寫到主內(nèi)存中的變量(非必須)
可見性
while (flag) {//語句1
doSomething();//語句2
}
flag = false;//語句3
順序性
if (inited == false) {
context = loadContext(); //語句1
inited = true; //語句2
}
doSomethingwithconfig(context); //語句3
JMM內(nèi)存模型
Java虛擬機(jī)規(guī)范定義了Java內(nèi)存模型(Java Memory Model,JMM)來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問效果(C/C++等則直接使用物理機(jī)和OS的內(nèi)存模型,使得程序須針對(duì)特定平臺(tái)編寫),它在多線程的情況下尤其重要。
內(nèi)存劃分

主內(nèi)存(Main Memory)存儲(chǔ)所有共享變量的值。
工作內(nèi)存(Working Memory)存儲(chǔ)該線程使用到的共享變量在主內(nèi)存的的值的副本拷貝。
這種劃分與Java內(nèi)存區(qū)域中堆、棧、方法區(qū)等的劃分是不同層次的劃分,兩者基本沒有關(guān)系。硬要聯(lián)系的話,大致上主內(nèi)存對(duì)應(yīng)Java堆中對(duì)象的實(shí)例數(shù)據(jù)部分、工作內(nèi)存對(duì)應(yīng)棧的部分區(qū)域;從更低層次上說,主內(nèi)存對(duì)應(yīng)物理硬件內(nèi)存、工作內(nèi)存對(duì)應(yīng)寄存器和高速緩存。
內(nèi)存間交互規(guī)則

lock: 將一個(gè)變量標(biāo)識(shí)為被一個(gè)線程獨(dú)占狀態(tài)
unclock: 將一個(gè)變量從獨(dú)占狀態(tài)釋放出來,釋放后的變量才可以被其他線程鎖定
read: 將一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中,以便隨后的load操作
load: 把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量的副本中
use: 把工作內(nèi)存中的一個(gè)變量的值傳給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)使用到變量的指令時(shí)都會(huì)使用該指令
assign: 把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存中的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的指令時(shí),都要使用該操作
store: 把工作內(nèi)存中的一個(gè)變量的值傳遞給主內(nèi)存,以便隨后的write操作
write: 把store操作從工作內(nèi)存中得到的變量的值寫到主內(nèi)存中的變量
不允許一個(gè)線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步會(huì)主內(nèi)存中
一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或者assign)的變量。即就是對(duì)一個(gè)變量實(shí)施use和store操作之前,必須先自行assign和load操作。
一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。lock和unlock必須成對(duì)出現(xiàn)。
如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量之前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
如果一個(gè)變量事先沒有被lock操作鎖定,則不允許對(duì)它執(zhí)行unlock操作;也不允許去unlock一個(gè)被其他線程鎖定的變量。
對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)
read、load、use必須成對(duì)順序出現(xiàn),但不要求連續(xù)出現(xiàn)。assign、store、write同之;
變量誕生和初始化:變量只能從主內(nèi)存“誕生”,且須先初始化后才能使用,即在use/store前須先load/assign;
lock一個(gè)變量后會(huì)清空工作內(nèi)存中該變量的值,使用前須先初始化;unlock前須將變量同步回主內(nèi)存;
一個(gè)變量同一時(shí)刻只能被一線程lock,lock幾次就須unlock幾次;未被lock的變量不允許被執(zhí)行unlock,一個(gè)線程不能去unlock其他線程lock的變量。
先行發(fā)生原則
Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何同步手段(volatile、synchronized等)就能夠得到保證的有序性,這個(gè)通常也稱為happens-before原則。
程序次序規(guī)則(Program Order Rule):一個(gè)線程內(nèi),邏輯上書寫在前面的操作先行發(fā)生于書寫在后面的操作。
鎖定規(guī)則(Monitor Lock Rule):一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。“后面”指時(shí)間上的先后順序。
volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。“后面”指時(shí)間上的先后順序。
傳遞規(guī)則(Transitivity):如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C。
線程啟動(dòng)規(guī)則(Thread Start Rule):Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作。
線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生(通過Thread.interrupted()檢測(cè))。
線程終止規(guī)則(Thread Termination Rule):線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行。
對(duì)象終結(jié)規(guī)則(Finaizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于他的finalize()方法的開始。
問題解決
原子性
由JMM直接保證的原子性變量操作包括read、load、use、assign、store、write;
基本數(shù)據(jù)類型的讀寫(工作內(nèi)存)是原子性的
可見性
“對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)”。
"如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量之前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值"
順序性
開發(fā)篇
volatile
對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。“后面”指時(shí)間上的先后順序
當(dāng)寫一個(gè) volatile 變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量刷新到主內(nèi)存。
當(dāng)讀一個(gè) volatile 變量時(shí),JMM 會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存置為無效,線程接下來將從主內(nèi)存中讀取共享變量。
對(duì)變量的寫入操作不依賴于其當(dāng)前值,即僅僅是讀取和單純的寫入,比如操作完成、中斷或者狀態(tài)之類的標(biāo)志
禁止對(duì)volatile變量操作指令的重排序
volatile底層是通過cpu提供的內(nèi)存屏障指令來實(shí)現(xiàn)的。硬件層的內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。
阻止屏障兩側(cè)的指令重排序
強(qiáng)制把寫緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫回主內(nèi)存,讓緩存中相應(yīng)的數(shù)據(jù)失效
final
寫final域的重排序規(guī)則:在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫入,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
讀final域的重排序規(guī)則:初次讀一個(gè)包含final域的對(duì)象的引用,與隨后初次讀這個(gè)final域,這兩個(gè)操作之間不能重排序。
public class FinalExample {
int i;//普通域
final int j;//final域
static FinalExample obj;
public FinalExample () {
i = 1;//寫普通域。對(duì)普通域的寫操作【可能會(huì)】被重排序到構(gòu)造函數(shù)之外
j = 2;//寫final域。對(duì)final域的寫操作【不會(huì)】被重排序到構(gòu)造函數(shù)之外
}
// 寫線程A執(zhí)行
public static void writer () { 
obj = new FinalExample ();
}
// 讀線程B執(zhí)行
public static void reader () { 
FinalExample object = obj;//讀對(duì)象引用
int a = object.i;//讀普通域。可能會(huì)看到結(jié)果為0(由于i=1可能被重排序到構(gòu)造函數(shù)外,此時(shí)y還沒有被初始化)
int b = object.j;//讀final域。保證能夠看到結(jié)果為2
}
}
在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final引用的對(duì)象的成員域的寫入,與隨后在構(gòu)造函數(shù)外把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序。
synchronized
確保代碼的同步執(zhí)行(即不同線程間的互斥)(原子性)
確保對(duì)共享變量的修改能夠及時(shí)可見(可見性)
有效解決指令重排問題(順序性)
進(jìn)入/加鎖時(shí)執(zhí)行字節(jié)碼指令MonitorEnter
退出/解鎖時(shí)執(zhí)行字節(jié)碼指令MonitorExit
當(dāng)執(zhí)行代碼有異常退出方法/代碼段時(shí),會(huì)自動(dòng)解鎖
修飾對(duì)象方法時(shí),使用當(dāng)前對(duì)象的監(jiān)視器
修飾靜態(tài)方法時(shí),使用類類型(Class 的對(duì)象)監(jiān)視器
修飾代碼塊時(shí),使用括號(hào)中的對(duì)象的監(jiān)視器
必須為 Object 類或其子類的對(duì)象
每個(gè)對(duì)象都有一個(gè)關(guān)聯(lián)的監(jiān)視器。
監(jiān)視器被鎖住,當(dāng)且僅當(dāng)它有屬主(Owner)時(shí)。
線程執(zhí)行MonitorEnter就是為了成為Monitor的屬主。
如果 Monitor 對(duì)象的記錄數(shù)(Entry Count,擁有它的線程的重入次數(shù))為 0, 將其置為 1,線程將自己置為 Monitor 對(duì)象的屬主。
如果Monitor的屬主為當(dāng)前線程,就會(huì)重入監(jiān)視器,將其記錄數(shù)增一。
如果Monitor的屬主為其它線程,當(dāng)前線程會(huì)阻塞,直到記錄數(shù)為0,才會(huì) 去競爭屬主權(quán)。
執(zhí)行MonitorExit的線程一定是這個(gè)對(duì)象所關(guān)聯(lián)的監(jiān)視器的屬主。
線程將Monitor對(duì)象的記錄數(shù)減一。
如果Monitor對(duì)象的記錄數(shù)為0,線程就會(huì)執(zhí)行退出動(dòng)作,不再是屬主。
此時(shí)其它阻塞的線程就被允許競爭屬主。
線程
關(guān)聯(lián)監(jiān)視器的對(duì)象
存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息
如果是數(shù)組的實(shí)例變量,還包括數(shù)組的長度
這部分內(nèi)存按4字節(jié)對(duì)齊
由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍
填充數(shù)據(jù)僅僅是為了字節(jié)對(duì)齊
保障下一個(gè)對(duì)象的起始地址為 8 的整數(shù)倍
長度可能為0
對(duì)象頭由 Mark Word 、Class Metadata Address(類元數(shù)據(jù)地址) 和 數(shù)組長度(對(duì)象為數(shù)組時(shí))組成
在 32 位和 64 位的虛擬機(jī)中,Mark Word 分別占用 32 字節(jié)和 64 字節(jié),因此稱其為 word

無鎖 -> 偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖

LIFO,單向鏈表
很多線程都可以把請(qǐng)求鎖的線程放入隊(duì)列中
但只有一個(gè)線程能將線程出隊(duì)
雙向鏈表
只有擁有鎖的線程才可以訪問或變更 EntryLis
只有擁有鎖的線程在釋放鎖時(shí),并且在 EntryList 為空、ContentionList 不為 空的情況下,才能將ContentionList 中的線程全部出隊(duì),放入到EntryList 中
將進(jìn)行 wait() 調(diào)用的線程放入WaitSet
當(dāng)進(jìn)行 notify()、notifyAll()調(diào)用時(shí),會(huì)將線程放入到ContentionList或EntryList 隊(duì)列中
對(duì)一個(gè)線程而言,在任何時(shí)候最多只處于三個(gè)集合中的一個(gè)
處于這三個(gè)集合中的線程,均為 BLOCKED 狀態(tài),底層使用互斥量來進(jìn)行阻塞
在獲取鎖時(shí),如果發(fā)生競爭,則使用自旋鎖來爭用,如果自旋后仍得不 到,再放入上述隊(duì)列中。
自旋可以減少ContentionList和EntryList上出隊(duì)入隊(duì)的操作,也就是減少了內(nèi)部 維護(hù)的這些鎖的爭用。
如果由1變?yōu)?,表示無競爭,繼續(xù)執(zhí)行
如果小于 0,表示有競爭,調(diào)用 futex(..., FUTEX_WAIT, ...) 使當(dāng)前線程休眠
如果futex變量由0變?yōu)?,表示無競爭,繼續(xù)執(zhí)行
如果 futex 變量變化前為負(fù)值,表示有競爭,調(diào)用 futex(..., FUTEX_WAKE, ...) 喚醒一個(gè)或多個(gè)等待線程
Lock
- END - 最近熱文
? 女友回老家了!沒吊事,手把手帶你搭建一臺(tái)服務(wù)器! ? 尼瑪,Github上最邪惡的開源項(xiàng)目了!未滿18或者女孩子勿進(jìn)哦~ ? 永別了,91網(wǎng)站!宣布永久關(guān)閉 ? 騰訊員工曬出薪資:真實(shí) 985 畢業(yè)薪資,大家看我還有救嗎?網(wǎng)友:日薪?
