接上篇關(guān)于編程路上的一些雜談 由線程的通信原理想到的(一),其實已經(jīng)討論一些鎖的實現(xiàn)了,這里再深入一下,把問題講明白。
底層實現(xiàn)原理
有volatile變量修飾的共享變量進(jìn)行寫操作的時候會多出第二行匯編代碼,通過查IA-32架構(gòu)軟件開發(fā)者手冊可知,Lock前綴的指令在多核處理器下會引發(fā)了兩件事情。
將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。
這個寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。
為了提高處理速度,處理器不直接和內(nèi)存進(jìn)行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進(jìn)行操作,但操作完不知道何時會寫到內(nèi)存。如果對聲明了volatile的變量進(jìn)行寫操作,JVM就會向處理器發(fā)送一條Lock前綴的指令,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。但是,就算寫回到內(nèi)存,如果其他處理器緩存的值還是舊的,再執(zhí)行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現(xiàn)緩存一致性協(xié)議,每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改,就會將當(dāng)前處理器的緩存行設(shè)置成無效狀態(tài),當(dāng)處理器對這個數(shù)據(jù)進(jìn)行修改操作的時候,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。
同樣,參照上面所說的,對于volatile來說,它的實現(xiàn)也不外乎需要達(dá)到以下兩種實現(xiàn)效果:
1)Lock前綴指令會引起處理器緩存回寫到內(nèi)存Lock前綴指令會引起處理器緩存回寫到內(nèi)存
2)一個處理器的緩存回寫到內(nèi)存會導(dǎo)致其他處理器的緩存無效
對象頭
對象頭:包括兩部分信息。第一部分用于存儲對象自身的運(yùn)行時數(shù)據(jù),如哈希碼,GC分代年齡、鎖狀態(tài)、線程持有鎖、等等。這部分?jǐn)?shù)據(jù)的長度在32為或64位,官方稱之為“MarkWord”。對象頭的另一部分是類型指針,即對象指向它的類元素的指針,通過這個指針來確定這個對象時那個類的實例。(如果Java對象時一個數(shù)組,則對象頭還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)。因為Java數(shù)組元數(shù)據(jù)中沒有數(shù)組大小的記錄)
偏向鎖的概念
HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當(dāng)一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用CAS競爭鎖;如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程。
進(jìn)入正題
在關(guān)于編程路上的一些雜談 由線程的通信原理想到的(一)其實已經(jīng)有講到volatile 的實現(xiàn)方式的,通過上面的深入想必已經(jīng)有更細(xì)致的了解,然后也相信大家對于像i++ 這種復(fù)合操作不具有原子性(i是volatile變量 )很是疑惑,這里要說一個概念:
程序計數(shù)器PC
程序計數(shù)器即指令地址寄存器。在某些計算機(jī)中用來存放當(dāng)前正在執(zhí)行的指令地址;而在另一些計算機(jī)中則用來存放即將要執(zhí)行的下一條指令地址;而在有指令預(yù)取功能的計算機(jī)中,一般還要增加一個程序計數(shù)器用來存放下一條要取出的指令地址。程序計數(shù)器用以指出下條指令在主存中的存放地址,CPU根據(jù)PC的內(nèi)容去主存取得指令。因程序中指令是順序執(zhí)行的,所以PC有自增功能。
也就是說其實i++可以理解成一條指令,而i=i+1便是兩條指令了包括i+1和將結(jié)果賦給i,應(yīng)該不需要我再深入了,已經(jīng)很明了了。
鎖的語義
這里在關(guān)于編程路上的一些雜談 由線程的通信原理想到的(一)已經(jīng)有說其底層還是依靠volatile來實現(xiàn),接下來就通過ReentrantLock源碼來具體對其進(jìn)行分析:
對于compareAndSetState來說:
CAS, CPU指令,在大多數(shù)處理器架構(gòu),包括IA32、Space中采用的都是CAS指令,CAS的語義是“我認(rèn)為V的值應(yīng)該為A,如果是,那么將V的值更新為B,否則不修改并告訴V的值實際為多少”,CAS是項 樂觀鎖 技術(shù),當(dāng)多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試。
CAS有3個操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值V修改為B,否則什么都不做。
對于compareAndSetState來說:它是個原子方法,原理就是是CAS.這個是高效,而且是原子的,不用加鎖. 也會因為其他值改了而產(chǎn)生誤操作,應(yīng)為會先判斷當(dāng)前值,符合期望才去改變,而我們所要操作的值無非就是state而已。
對于上面截圖的代碼說的直白點就是對于一個線程如果當(dāng)前沒有競爭,則直接拿到或者上鎖,否則,嘗試獲取即acquire(1)方法:
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先通過tryAcquire()方法嘗試獲取,如果不能的話,則通過AddWaiter()方法,用當(dāng)前線程生成一個Node放入隊尾,而acquireQueued()則是一種自旋鎖的實現(xiàn)方式。最后把當(dāng)前線程interrupt。這里可以發(fā)現(xiàn),java的 AQS的實現(xiàn)很巧妙的一個地方就是把tryAcquire延遲到子類去實現(xiàn)。公平鎖和非公平鎖的實現(xiàn)方式是不一樣的。非公平鎖的tryAcquire()的是通過nonfairTryAcquire()。
然后看acquireQueued(),其實就是一個無限循環(huán),直到獲得鎖為止。通過上圖源碼可以看到在shouldParkAfterFailedAcquire()方法中,通過前一個Node的waitStatus來判斷是否應(yīng)該把當(dāng)前線程阻塞(所以用了雙&&開關(guān)語義),阻塞是通過parkAndCheckInterrupt()中的LockSupport.park()實現(xiàn)。
再看一下釋放鎖:
/**
* Attempts to release this lock.
*
* <p>If the current thread is the holder of this lock then the hold
* count is decremented. If the hold count is now zero then the lock
* is released. If the current thread is not the holder of this
* lock then {@link IllegalMonitorStateException} is thrown.
*
* @throws IllegalMonitorStateException if the current thread does not
* hold this lock
*/
public void unlock() {
sync.release(1);
}
release:
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可以看出tryRelease和tryAcquire一樣,也是延遲到子類(Sync)實現(xiàn)的。c==0的時候,才能成功釋放鎖,所以多次鎖定(看源碼就可以知道lock一次c就+1,第一張截圖的第二個判斷,假如是當(dāng)前線程的話就再+一次1)就需要多次釋放才能解鎖。釋放鎖之后,就會喚醒隊列的一個node中的線程
這段代碼目的在于找出第一個可以unpark的線程,一般說來head.next == head,Head就是第一個線程,但Head.next可能會被置為null(參考acquireQueued()源碼),因此比較穩(wěn)妥的辦法是從后往前找第一個可用線程。
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
其實我們在設(shè)計代碼的時候也是可以通過靜態(tài)內(nèi)部類的方式來實現(xiàn)一些自己想要的功能,不過我們經(jīng)常會用Spring框架,其通過動態(tài)代理已經(jīng)實現(xiàn)了這個按需的延遲加載這些特性,也無須去頭疼這些那些的。
其實關(guān)鍵點也就這些,繞來繞去其實就一句話,假如有A和B兩個線程,A符合期望的話,那么A就可以入主東宮了,B還老老實實的做它的嬪妃就是。
通過以上這些解釋,其實我們發(fā)現(xiàn),鎖的底層其實也是在反復(fù)操作一個volatile 變量,而多線程的其他操作也是基于volatile 的特性來實現(xiàn)的,包括計數(shù)器,barrier,各種安全工具類,理解這個其他自然都不是什么問題,包括很多并發(fā)框架的和事務(wù)等的設(shè)計,先就扯到這里吧。
參考文獻(xiàn):
Java并發(fā)編程的藝術(shù)
在學(xué)習(xí)過程如果有任何疑問,請來極樂網(wǎng)
提問,或者掃描下方二維碼,關(guān)注極樂官方微信,在平臺下方留言。