異常解析
在線程中調(diào)用wait方法的時(shí)候要用synchronized鎖住對(duì)象陆淀,確保代碼段不會(huì)被多個(gè)線程調(diào)用。
如果沒有synchronized加鎖系吩,那么當(dāng)前的線程不是此對(duì)象監(jiān)視器的所有者, 就會(huì)拋出 IllegalMonitorStateException 異常信息玖姑。
當(dāng)前線程要鎖定該對(duì)象之后社付,才能用鎖定的對(duì)象執(zhí)行這些方法,這里需要用到synchronized關(guān)鍵字燃箭,鎖定哪個(gè)對(duì)象就用哪個(gè)對(duì)象來執(zhí)行 notify(), notifyAll(),wait(), wait(long), wait(long, int) 操作冲呢,否則就會(huì)報(bào)IllegalMonitorStateException異常。
在JVM中招狸,每個(gè)對(duì)象和類在邏輯上都是和一個(gè)監(jiān)視器相關(guān)聯(lián)的敬拓。為了實(shí)現(xiàn)監(jiān)視器的排他性監(jiān)視能力,JVM為每一個(gè)對(duì)象和類都關(guān)聯(lián)一個(gè)鎖裙戏。鎖住了一個(gè)對(duì)象乘凸,就是獲得對(duì)象相關(guān)聯(lián)的監(jiān)視器。
監(jiān)視器好比一做建筑,它有一個(gè)很特別的房間,房間里有一些數(shù)據(jù),而且在同一時(shí)間只能被一個(gè)線程占據(jù),進(jìn)入這個(gè)建筑叫做"進(jìn)入監(jiān)視器",進(jìn)入建筑中的那個(gè)特別的房間叫做"獲得監(jiān)視器",占據(jù)房間叫做"持有監(jiān)視器",離開房間叫做"釋放監(jiān)視器",離開建筑叫做"退出監(jiān)視器"累榜。
而一個(gè)鎖就像一種任何時(shí)候只允許一個(gè)線程擁有的特權(quán)营勤。一個(gè)線程可以允許多次對(duì)同一對(duì)象上鎖.對(duì)于每一個(gè)對(duì)象來說,java虛擬機(jī)維護(hù)一個(gè)計(jì)數(shù)器,記錄對(duì)象被加了多少次鎖,沒被鎖的對(duì)象的計(jì)數(shù)器是0,線程每加鎖一次,計(jì)數(shù)器就加1,每釋放一次,計(jì)數(shù)器就減1.當(dāng)計(jì)數(shù)器跳到0的時(shí)候,鎖就被完全釋放了。
Java虛擬機(jī)中的一個(gè)線程在它到達(dá)監(jiān)視區(qū)域開始處的時(shí)候請(qǐng)求一個(gè)鎖.JAVA程序中每一個(gè)監(jiān)視區(qū)域都和一個(gè)對(duì)象引用相關(guān)聯(lián). 在java中壹罚,synchronized是唯一實(shí)現(xiàn)同步的東西葛作。
Java對(duì)象的組成與鎖的狀態(tài)
HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為三塊區(qū)域:
- 對(duì)象頭(Header)
- 實(shí)例數(shù)據(jù)(Instance Data)和
- 對(duì)齊填充(Padding)
HotSpot虛擬機(jī)的對(duì)象頭(Object Header)包括兩部分信息渔嚷,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)进鸠, 如哈希碼(HashCode)、GC分代年齡形病、鎖狀態(tài)標(biāo)志客年、線程持有的鎖霞幅、偏向線程ID、偏向時(shí)間戳等等量瓜,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)(暫 不考慮開啟壓縮指針的場(chǎng)景)中分別為32個(gè)和64個(gè)Bits司恳,官方稱它為“Mark Word”。
對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多绍傲,其實(shí)已經(jīng)超出了32扔傅、64位Bitmap結(jié)構(gòu)所能記錄的限度,但是對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無關(guān)的額 外存儲(chǔ)成本烫饼,考慮到虛擬機(jī)的空間效率猎塞,Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間杠纵。例如在32位的HotSpot虛擬機(jī) 中對(duì)象未被鎖定的狀態(tài)下荠耽,Mark Word的32個(gè)Bits空間中的25Bits用于存儲(chǔ)對(duì)象哈希碼(HashCode),4Bits用于存儲(chǔ)對(duì)象分代年齡比藻,2Bits用于存儲(chǔ)鎖標(biāo)志 位铝量,1Bit固定為0,在其他狀態(tài)(輕量級(jí)鎖定银亲、重量級(jí)鎖定慢叨、GC標(biāo)記、可偏向)下對(duì)象的存儲(chǔ)內(nèi)容如下表所示务蝠。
鎖的狀態(tài)總共有四種:無鎖狀態(tài)拍谐、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖请梢。隨著鎖的競爭赠尾,鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖(但是鎖的升級(jí)是單向的毅弧,也就是說只能從低到高升級(jí)气嫁,不會(huì)出現(xiàn)鎖的降級(jí))。JDK 1.6中默認(rèn)是開啟偏向鎖和輕量級(jí)鎖的够坐,我們也可以通過-XX:-UseBiasedLocking來禁用偏向鎖寸宵。
Mark Word
Mark Word記錄了對(duì)象和鎖有關(guān)的信息,當(dāng)這個(gè)對(duì)象被synchronized關(guān)鍵字當(dāng)成同步鎖時(shí)元咙,圍繞這個(gè)鎖的一系列操作都和Mark Word有關(guān)梯影。
Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit庶香。
Mark Word在不同的鎖狀態(tài)下存儲(chǔ)的內(nèi)容不同甲棍,在32位JVM中是這么存的:
其中無鎖和偏向鎖的鎖標(biāo)志位都是01,只是在前面的1bit區(qū)分了這是無鎖狀態(tài)還是偏向鎖狀態(tài)赶掖。
JDK1.6以后的版本在處理同步鎖時(shí)存在鎖升級(jí)的概念感猛,JVM對(duì)于同步鎖的處理是從偏向鎖開始的七扰,隨著競爭越來越激烈,處理方式從偏向鎖升級(jí)到輕量級(jí)鎖陪白,最終升級(jí)到重量級(jí)鎖颈走。
JVM一般是這樣使用鎖和Mark Word的:
1,當(dāng)沒有被當(dāng)成鎖時(shí)咱士,這就是一個(gè)普通的對(duì)象立由,Mark Word記錄對(duì)象的HashCode,鎖標(biāo)志位是01序厉,是否偏向鎖那一位是0锐膜。
2,當(dāng)對(duì)象被當(dāng)做同步鎖并有一個(gè)線程A搶到了鎖時(shí)脂矫,鎖標(biāo)志位還是01枣耀,但是否偏向鎖那一位改成1霉晕,前23bit記錄搶到鎖的線程id庭再,表示進(jìn)入偏向鎖狀態(tài)。
3牺堰,當(dāng)線程A再次試圖來獲得鎖時(shí)拄轻,JVM發(fā)現(xiàn)同步鎖對(duì)象的標(biāo)志位是01,是否偏向鎖是1伟葫,也就是偏向狀態(tài)恨搓,Mark Word中記錄的線程id就是線程A自己的id,表示線程A已經(jīng)獲得了這個(gè)偏向鎖筏养,可以執(zhí)行同步鎖的代碼斧抱。
4,當(dāng)線程B試圖獲得這個(gè)鎖時(shí)渐溶,JVM發(fā)現(xiàn)同步鎖處于偏向狀態(tài)辉浦,但是Mark Word中的線程id記錄的不是B,那么線程B會(huì)先用CAS操作試圖獲得鎖茎辐,這里的獲得鎖操作是有可能成功的宪郊,因?yàn)榫€程A一般不會(huì)自動(dòng)釋放偏向鎖。如果搶鎖成功拖陆,就把Mark Word里的線程id改為線程B的id弛槐,代表線程B獲得了這個(gè)偏向鎖,可以執(zhí)行同步鎖代碼依啰。如果搶鎖失敗乎串,則繼續(xù)執(zhí)行步驟5。
5速警,偏向鎖狀態(tài)搶鎖失敗叹誉,代表當(dāng)前鎖有一定的競爭艰争,偏向鎖將升級(jí)為輕量級(jí)鎖。JVM會(huì)在當(dāng)前線程的線程棧中開辟一塊單獨(dú)的空間桂对,里面保存指向?qū)ο箧iMark Word的指針甩卓,同時(shí)在對(duì)象鎖Mark Word中保存指向這片空間的指針。上述兩個(gè)保存操作都是CAS操作蕉斜,如果保存成功逾柿,代表線程搶到了同步鎖,就把Mark Word中的鎖標(biāo)志位改成00宅此,可以執(zhí)行同步鎖代碼机错。如果保存失敗,表示搶鎖失敗父腕,競爭太激烈弱匪,繼續(xù)執(zhí)行步驟6。
6璧亮,輕量級(jí)鎖搶鎖失敗萧诫,JVM會(huì)使用自旋鎖,自旋鎖不是一個(gè)鎖狀態(tài)枝嘶,只是代表不斷的重試帘饶,嘗試搶鎖。從JDK1.7開始群扶,自旋鎖默認(rèn)啟用及刻,自旋次數(shù)由JVM決定。如果搶鎖成功則執(zhí)行同步鎖代碼竞阐,如果失敗則繼續(xù)執(zhí)行步驟7缴饭。
7,自旋鎖重試之后如果搶鎖依然失敗骆莹,同步鎖會(huì)升級(jí)至重量級(jí)鎖颗搂,鎖標(biāo)志位改為10。在這個(gè)狀態(tài)下汪疮,未搶到鎖的線程都會(huì)被阻塞峭火。
指向類的指針
該指針在32位JVM中的長度是32bit,在64位JVM中長度是64bit智嚷。
Java對(duì)象的類數(shù)據(jù)保存在方法區(qū)卖丸。
數(shù)組長度
只有數(shù)組對(duì)象保存了這部分?jǐn)?shù)據(jù)。
該數(shù)據(jù)在32位和64位JVM中長度都是32bit盏道。
實(shí)例數(shù)據(jù)
對(duì)象的實(shí)例數(shù)據(jù)就是在java代碼中能看到的屬性和他們的值稍浆。
對(duì)齊填充字節(jié)
因?yàn)镴VM要求java的對(duì)象占的內(nèi)存大小應(yīng)該是8bit的倍數(shù),所以后面有幾個(gè)字節(jié)用于把對(duì)象的大小補(bǔ)齊至8bit的倍數(shù),沒有特別的功能衅枫。
Java對(duì)象的Monitor機(jī)制
Monitor的機(jī)制分析
Java虛擬機(jī)給每個(gè)對(duì)象和class字節(jié)碼都設(shè)置了一個(gè)監(jiān)聽器Monitor嫁艇,用于檢測(cè)并發(fā)代碼的重入,同時(shí)在Object類中還提供了notify和wait方法來對(duì)線程進(jìn)行控制弦撩。
在java.lang.Object類中有如下代碼:
public class Object {
...
private transient int shadow$_monitor_;
public final native void notify();
public final native void notifyAll();
public final native void wait() throws InterruptedException;
public final void wait(long millis) throws InterruptedException {
wait(millis, 0);
}
public final native void wait(long millis, int nanos) throws InterruptedException;
...
}
Monitor的機(jī)制如下圖:
結(jié)合上圖來分析Object的Monitor機(jī)制步咪。
Monitor可以類比為一個(gè)特殊的房間,這個(gè)房間中有一些被保護(hù)的數(shù)據(jù)益楼,Monitor保證每次只能有一個(gè)線程能進(jìn)入這個(gè)房間進(jìn)行訪問被保護(hù)的數(shù)據(jù)猾漫,進(jìn)入房間即為持有Monitor,退出房間即為釋放Monitor感凤。
當(dāng)一個(gè)線程需要訪問受保護(hù)的數(shù)據(jù)(即需要獲取對(duì)象的Monitor)時(shí)悯周,它會(huì)首先在entry-set入口隊(duì)列中排隊(duì)(這里并不是真正的按照排隊(duì)順序),如果沒有其他線程正在持有對(duì)象的Monitor陪竿,那么它會(huì)和entry-set隊(duì)列和wait-set隊(duì)列中的被喚醒的其他線程進(jìn)行競爭(即通過CPU調(diào)度)禽翼,選出一個(gè)線程來獲取對(duì)象的Monitor,執(zhí)行受保護(hù)的代碼段族跛,執(zhí)行完畢后釋放Monitor闰挡,如果已經(jīng)有線程持有對(duì)象的Monitor,那么需要等待其釋放Monitor后再進(jìn)行競爭庸蔼。
再說一下wait-set隊(duì)列解总。當(dāng)一個(gè)線程擁有Monitor后,經(jīng)過某些條件的判斷(比如用戶取錢發(fā)現(xiàn)賬戶沒錢)姐仅,這個(gè)時(shí)候需要調(diào)用Object的wait方法,線程就釋放了Monitor刻盐,進(jìn)入wait-set隊(duì)列掏膏,等待Object的notify方法(比如用戶向賬戶里面存錢)。當(dāng)該對(duì)象調(diào)用了notify方法或者notifyAll方法后敦锌,wait-set中的線程就會(huì)被喚醒馒疹,然后在wait-set隊(duì)列中被喚醒的線程和entry-set隊(duì)列中的線程一起通過CPU調(diào)度來競爭對(duì)象的Monitor,最終只有一個(gè)線程能獲取對(duì)象的Monitor乙墙。
需要注意的是:
當(dāng)一個(gè)線程在wait-set中被喚醒后颖变,并不一定會(huì)立刻獲取Monitor,它需要和其他線程去競爭;
如果一個(gè)線程是從wait-set隊(duì)列中喚醒后听想,獲取到的Monitor腥刹,它會(huì)去讀取它自己保存的PC計(jì)數(shù)器中的地址谭企,從它調(diào)用wait方法的地方開始執(zhí)行纺阔。
Monitor機(jī)制在Java中是如何實(shí)現(xiàn)的呢?
即通過synchronized關(guān)鍵字實(shí)現(xiàn)線程同步來獲取對(duì)象的Monitor症昏。synchronized同步分為以下兩種方式:
同步代碼塊:
synchronized(Obejct obj) {
//同步代碼塊
...
}
上述代碼表示在進(jìn)入同步代碼塊之前,先要去獲取obj的Monitor垫卤,如果已經(jīng)被其他線程獲取了威彰,那么當(dāng)前線程必須等待直至其他線程釋放obj的Monitor
這里的obj可以是類.class,表示需要去獲取該類的字節(jié)碼的Monitor穴肘,獲取后歇盼,其他線程無法再去獲取到class字節(jié)碼的Monitor了,即無法訪問屬于類的同步的靜態(tài)方法了评抚,但是對(duì)于對(duì)象的實(shí)例方法的訪問不受影響
同步方法:
public class Test {
public static Test instance;
public int val;
public synchronized void set(int val) {
this.val = val;
}
public static synchronized void set(Test instance) {
Test.instance = instance;
}
}
上述使用了synchronized分別修飾了非靜態(tài)方法和靜態(tài)方法旺遮。
非靜態(tài)方法可以理解為,需要獲取當(dāng)前對(duì)象this的Monitor盈咳,獲取后耿眉,其他需要獲取該對(duì)象的Monitor的線程會(huì)被堵塞。
靜態(tài)方法可以理解為鱼响,需要獲取該類字節(jié)碼的Monitor(因?yàn)閟tatic方法不屬于任何對(duì)象鸣剪,而是屬于類的方法),獲取后丈积,其他需要獲取字節(jié)碼的Monitor的線程會(huì)被堵塞筐骇。
Object的notify方法和wait方法詳解
上面在講述Monitor機(jī)制的時(shí)候已經(jīng)分析了notify和wait的用法,這里具體分析下江滨。
wait方法
wait有三個(gè)重載方法铛纬,分別如下:
public final void wait() throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException;
后面兩個(gè)傳入了時(shí)間參數(shù)(nanos表示納秒),表示如果指定時(shí)間過去還沒有其他線程調(diào)用notify或者notifyAll方法來將其喚醒唬滑,那么該線程會(huì)自動(dòng)被喚醒告唆。
調(diào)用obj.wait方法需要注意的是,當(dāng)前線程必須獲取到了obj的Monitor(The current thread must own this object's monitor)晶密,才能去調(diào)用其wait方法擒悬,即wait必須放在同步方法或同步代碼塊中。
調(diào)用的是obj.wait()稻艰,而不是Thread.currentThread.wait()懂牧。
notify() 方法
/**
* Wakes up a single thread that is waiting on this object's
* monitor. If any threads are waiting on this object, one of them
* is chosen to be awakened.
* ...
* Only one thread at a time can own an object's monitor.
*
* @throws IllegalMonitorStateException if the current thread is not
* the owner of this object's monitor.
* @see java.lang.Object#notifyAll()
* @see java.lang.Object#wait()
*/
public final native void notify();
notify有兩個(gè)方法notify和notifyAll,前者只能喚醒一個(gè)正在等待這個(gè)對(duì)象的monitor的線程尊勿,具體由JVM決定僧凤,后者則會(huì)喚醒所有正在等待這個(gè)對(duì)象的monitor的線程
需要關(guān)注的點(diǎn):
調(diào)用notify方法,并不意味著釋放了Monitor元扔,必須要等同步代碼塊結(jié)束后才會(huì)釋放Monitor躯保。
在調(diào)用notify方法時(shí),必須保證其他線程處于wait狀態(tài)摇展,否則調(diào)用notify沒有任何效果吻氧,導(dǎo)致之后其他線程永遠(yuǎn)處于堵塞狀態(tài)溺忧。
notify()
Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object's monitor by calling one of the wait methods.
喚醒正在此對(duì)象監(jiān)視器上等待的單個(gè)線程。如果有任何線程正在等待這個(gè)對(duì)象盯孙,那么將選擇喚醒其中的一個(gè)線程鲁森。這個(gè)選擇是任意的,由實(shí)現(xiàn)決定振惰。線程通過調(diào)用其中一個(gè)等待方法來等待對(duì)象的監(jiān)視器歌溉。
The awakened thread will not be able to proceed until the current thread relinquishes(釋放) the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened thread enjoys no reliable privilege or disadvantage in being the next thread to lock this object.
被喚醒的線程將無法繼續(xù)前進(jìn),直到當(dāng)前線程釋放該對(duì)象上的鎖骑晶。被喚醒的線程將以通常的方式與其他線程競爭痛垛,這些線程可能正在積極地對(duì)這個(gè)對(duì)象進(jìn)行同步; 例如,在成為下一個(gè)鎖定此對(duì)象的線程時(shí)桶蛔,被喚醒的線程沒有任何特權(quán)或不利條件匙头。
This method should only be called by a thread that is the owner of this object's monitor. A thread becomes the owner of the object's monitor in one of three ways:
- By executing a synchronized instance method of that object.
- By executing the body of a synchronized statement that synchronizes on the object.
此方法只能由此對(duì)象監(jiān)視器的所有者的線程調(diào)用。線程通過以下三種方式之一成為對(duì)象監(jiān)視器的所有者:
- 通過執(zhí)行該對(duì)象的同步實(shí)例方法仔雷。
- 通過執(zhí)行同步語句的主體對(duì)對(duì)象進(jìn)行同步蹂析。
For objects of type Class, by executing a synchronized static method of that class.
Only one thread at a time can own an object's monitor.
對(duì)于Class類型的對(duì)象,通過執(zhí)行該類的同步靜態(tài)方法碟婆。
每次只有一個(gè)線程可以擁有一個(gè)對(duì)象的監(jiān)視器电抚。
Throws:
IllegalMonitorStateException
– if the current thread is not the owner of this object's monitor.
See Also:
notifyAll(), wait()
小結(jié)
java中每個(gè)對(duì)象都有唯一的一個(gè)monitor,想擁有一個(gè)對(duì)象的monitor的話有以下三種方式:
1.執(zhí)行該對(duì)象的同步方法
public synchronize a () {
}
2.執(zhí)行該對(duì)象的同步塊
synchronize(obj) {
}
3.執(zhí)行某個(gè)類的靜態(tài)同步方法
public static synchronize b(){
}
Tips:擁有monitor的是線程竖共。
1.同時(shí)只能有一個(gè)線程可以獲取某個(gè)對(duì)象的monitor
2.一個(gè)線程通過調(diào)用某個(gè)對(duì)象的wait()方法釋放該對(duì)象的monitor并進(jìn)入休眠狀態(tài)蝙叛,直到其他線程獲取了被該線程釋放的monitor并調(diào)用該對(duì)象的notify()或者notifyAll()后再次競爭獲取該對(duì)象的monitor
3.只有擁有該對(duì)象monitor的線程才可以調(diào)用該對(duì)象的notify()和notifyAll()方法
如果沒有該對(duì)象monitor的線程調(diào)用了該對(duì)象的notify()或者notifyAll()方法將會(huì)拋出java.lang.IllegalMonitorStateException。
參考資料
https://blog.csdn.net/lkforce/article/details/81128115
https://blog.csdn.net/boyeleven/article/details/81390738
https://blog.csdn.net/hqq2023623/article/details/51000153
Kotlin開發(fā)者社區(qū)
專注分享 Java公给、 Kotlin借帘、Spring/Spring Boot、MySQL妓布、redis姻蚓、neo4j、NoSQL匣沼、Android、JavaScript捂龄、React释涛、Node、函數(shù)式編程倦沧、編程思想唇撬、"高可用,高性能展融,高實(shí)時(shí)"大型分布式系統(tǒng)架構(gòu)設(shè)計(jì)主題窖认。
High availability, high performance, high real-time large-scale distributed system architecture design。
分布式框架:Zookeeper、分布式中間件框架等
分布式存儲(chǔ):GridFS扑浸、FastDFS烧给、TFS、MemCache喝噪、redis等
分布式數(shù)據(jù)庫:Cobar础嫡、tddl、Amoeba酝惧、Mycat
云計(jì)算榴鼎、大數(shù)據(jù)、AI算法
虛擬化晚唇、云原生技術(shù)
分布式計(jì)算框架:MapReduce巫财、Hadoop、Storm哩陕、Flink等
分布式通信機(jī)制:Dubbo平项、RPC調(diào)用、共享遠(yuǎn)程數(shù)據(jù)萌踱、消息隊(duì)列等
消息隊(duì)列MQ:Kafka葵礼、MetaQ,RocketMQ
怎樣打造高可用系統(tǒng):基于硬件并鸵、軟件中間件鸳粉、系統(tǒng)架構(gòu)等一些典型方案的實(shí)現(xiàn):HAProxy、基于Corosync+Pacemaker的高可用集群套件中間件系統(tǒng)
Mycat架構(gòu)分布式演進(jìn)
大數(shù)據(jù)Join背后的難題:數(shù)據(jù)园担、網(wǎng)絡(luò)届谈、內(nèi)存和計(jì)算能力的矛盾和調(diào)和
Java分布式系統(tǒng)中的高性能難題:AIO,NIO弯汰,Netty還是自己開發(fā)框架艰山?
高性能事件派發(fā)機(jī)制:線程池模型、Disruptor模型等等咏闪。曙搬。。
合抱之木鸽嫂,生于毫末纵装;九層之臺(tái),起于壘土据某;千里之行橡娄,始于足下。不積跬步癣籽,無以至千里挽唉;不積小流滤祖,無以成江河。