接上篇關(guān)于編程路上的一些雜談 由線程的通信原理想到的(一),其實已經(jīng)討論一些鎖的實現(xiàn)了狂男,這里再深入一下,把問題講明白。
底層實現(xiàn)原理
有volatile變量修飾的共享變量進行寫操作的時候會多出第二行匯編代碼煮仇,通過查IA-32架構(gòu)軟件開發(fā)者手冊可知,Lock前綴的指令在多核處理器下會引發(fā)了兩件事情谎仲。
將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存浙垫。
這個寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。
為了提高處理速度郑诺,處理器不直接和內(nèi)存進行通信绞呈,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進行操作间景,但操作完不知道何時會寫到內(nèi)存佃声。如果對聲明了volatile的變量進行寫操作,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ù)進行修改操作的時候评甜,會重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里灰粮。
同樣,參照上面所說的忍坷,對于volatile來說粘舟,它的實現(xiàn)也不外乎需要達(dá)到以下兩種實現(xiàn)效果:
1)Lock前綴指令會引起處理器緩存回寫到內(nèi)存Lock前綴指令會引起處理器緩存回寫到內(nèi)存
2)一個處理器的緩存回寫到內(nèi)存會導(dǎo)致其他處理器的緩存無效
對象頭
對象頭:包括兩部分信息。第一部分用于存儲對象自身的運行時數(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娱挨,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖捕犬。如果測試成功跷坝,表示線程已經(jīng)獲得了鎖。如果測試失敗碉碉,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置柴钻,則使用CAS競爭鎖;如果設(shè)置了垢粮,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程贴届。
進入正題
在關(guān)于編程路上的一些雜談 由線程的通信原理想到的(一)其實已經(jīng)有講到volatile 的實現(xiàn)方式的,通過上面的深入想必已經(jīng)有更細(xì)致的了解,然后也相信大家對于像i++ 這種復(fù)合操作不具有原子性(i是volatile變量 )很是疑惑毫蚓,這里要說一個概念:
程序計數(shù)器PC
程序計數(shù)器即指令地址寄存器占键。在某些計算機中用來存放當(dāng)前正在執(zhí)行的指令地址;而在另一些計算機中則用來存放即將要執(zhí)行的下一條指令地址绍些;而在有指令預(yù)取功能的計算機中捞慌,一般還要增加一個程序計數(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源碼來具體對其進行分析:


對于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è)計,先就扯到這里吧俺附。
參考文獻:
Java并發(fā)編程的藝術(shù)
在學(xué)習(xí)過程如果有任何疑問肥卡,請來極樂網(wǎng)
提問溪掀,或者掃描下方二維碼事镣,關(guān)注極樂官方微信,在平臺下方留言揪胃。
