IllegalMonitorStateException 異常 與 Java中的"對象監視器Monitor"和"對象鎖"詳解

異常解析

在線程中調用wait方法的時候要用synchronized鎖住對象,確保代碼段不會被多個線程調用。

如果沒有synchronized加鎖,那麼當前的線程不是此對象監視器的所有者, 就會拋出 IllegalMonitorStateException 異常信息。

當前線程要鎖定該對象之後,才能用鎖定的對象執行這些方法,這裏需要用到synchronized關鍵字,鎖定哪個對象就用哪個對象來執行 notify(), notifyAll(),wait(), wait(long), wait(long, int) 操作,否則就會報IllegalMonitorStateException異常。

在JVM中,每個對象和類在邏輯上都是和一個監視器相關聯的。爲了實現監視器的排他性監視能力,JVM爲每一個對象和類都關聯一個鎖。鎖住了一個對象,就是獲得對象相關聯的監視器。

監視器好比一做建築,它有一個很特別的房間,房間裏有一些數據,而且在同一時間只能被一個線程佔據,進入這個建築叫做"進入監視器",進入建築中的那個特別的房間叫做"獲得監視器",佔據房間叫做"持有監視器",離開房間叫做"釋放監視器",離開建築叫做"退出監視器"。

而一個鎖就像一種任何時候只允許一個線程擁有的特權。一個線程可以允許多次對同一對象上鎖.對於每一個對象來說,java虛擬機維護一個計數器,記錄對象被加了多少次鎖,沒被鎖的對象的計數器是0,線程每加鎖一次,計數器就加1,每釋放一次,計數器就減1.當計數器跳到0的時候,鎖就被完全釋放了。

Java虛擬機中的一個線程在它到達監視區域開始處的時候請求一個鎖.JAVA程序中每一個監視區域都和一個對象引用相關聯. 在java中,synchronized是唯一實現同步的東西。

Java對象的組成與鎖的狀態

HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲三塊區域:

  • 對象頭(Header)
  • 實例數據(Instance Data)和
  • 對齊填充(Padding)

HotSpot虛擬機的對象頭(Object Header)包括兩部分信息,第一部分用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等,這部分數據的長度在32位和64位的虛擬機(暫 不考慮開啓壓縮指針的場景)中分別爲32個和64個Bits,官方稱它爲“Mark Word”。

對象需要存儲的運行時數據很多,其實已經超出了32、64位Bitmap結構所能記錄的限度,但是對象頭信息是與對象自身定義的數據無關的額 外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘量多的信息,它會根據對象的狀態複用自己的存儲空間。例如在32位的HotSpot虛擬機 中對象未被鎖定的狀態下,Mark Word的32個Bits空間中的25Bits用於存儲對象哈希碼(HashCode),4Bits用於存儲對象分代年齡,2Bits用於存儲鎖標誌 位,1Bit固定爲0,在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容如下表所示。

鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中默認是開啓偏向鎖和輕量級鎖的,我們也可以通過-XX:-UseBiasedLocking來禁用偏向鎖。

Mark Word

Mark Word記錄了對象和鎖有關的信息,當這個對象被synchronized關鍵字當成同步鎖時,圍繞這個鎖的一系列操作都和Mark Word有關。

Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

Mark Word在不同的鎖狀態下存儲的內容不同,在32位JVM中是這麼存的:

其中無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態還是偏向鎖狀態。

JDK1.6以後的版本在處理同步鎖時存在鎖升級的概念,JVM對於同步鎖的處理是從偏向鎖開始的,隨着競爭越來越激烈,處理方式從偏向鎖升級到輕量級鎖,最終升級到重量級鎖。

JVM一般是這樣使用鎖和Mark Word的:

1,當沒有被當成鎖時,這就是一個普通的對象,Mark Word記錄對象的HashCode,鎖標誌位是01,是否偏向鎖那一位是0。

2,當對象被當做同步鎖並有一個線程A搶到了鎖時,鎖標誌位還是01,但是否偏向鎖那一位改成1,前23bit記錄搶到鎖的線程id,表示進入偏向鎖狀態。

3,當線程A再次試圖來獲得鎖時,JVM發現同步鎖對象的標誌位是01,是否偏向鎖是1,也就是偏向狀態,Mark Word中記錄的線程id就是線程A自己的id,表示線程A已經獲得了這個偏向鎖,可以執行同步鎖的代碼。

4,當線程B試圖獲得這個鎖時,JVM發現同步鎖處於偏向狀態,但是Mark Word中的線程id記錄的不是B,那麼線程B會先用CAS操作試圖獲得鎖,這裏的獲得鎖操作是有可能成功的,因爲線程A一般不會自動釋放偏向鎖。如果搶鎖成功,就把Mark Word裏的線程id改爲線程B的id,代表線程B獲得了這個偏向鎖,可以執行同步鎖代碼。如果搶鎖失敗,則繼續執行步驟5。

5,偏向鎖狀態搶鎖失敗,代表當前鎖有一定的競爭,偏向鎖將升級爲輕量級鎖。JVM會在當前線程的線程棧中開闢一塊單獨的空間,裏面保存指向對象鎖Mark Word的指針,同時在對象鎖Mark Word中保存指向這片空間的指針。上述兩個保存操作都是CAS操作,如果保存成功,代表線程搶到了同步鎖,就把Mark Word中的鎖標誌位改成00,可以執行同步鎖代碼。如果保存失敗,表示搶鎖失敗,競爭太激烈,繼續執行步驟6。

6,輕量級鎖搶鎖失敗,JVM會使用自旋鎖,自旋鎖不是一個鎖狀態,只是代表不斷的重試,嘗試搶鎖。從JDK1.7開始,自旋鎖默認啓用,自旋次數由JVM決定。如果搶鎖成功則執行同步鎖代碼,如果失敗則繼續執行步驟7。

7,自旋鎖重試之後如果搶鎖依然失敗,同步鎖會升級至重量級鎖,鎖標誌位改爲10。在這個狀態下,未搶到鎖的線程都會被阻塞。

指向類的指針

該指針在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

Java對象的類數據保存在方法區。

數組長度

只有數組對象保存了這部分數據。

該數據在32位和64位JVM中長度都是32bit。

實例數據

對象的實例數據就是在java代碼中能看到的屬性和他們的值。

對齊填充字節

因爲JVM要求java的對象佔的內存大小應該是8bit的倍數,所以後面有幾個字節用於把對象的大小補齊至8bit的倍數,沒有特別的功能。

Java對象的Monitor機制

Monitor的機制分析

Java虛擬機給每個對象和class字節碼都設置了一個監聽器Monitor,用於檢測併發代碼的重入,同時在Object類中還提供了notify和wait方法來對線程進行控制。

在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的機制如下圖:

結合上圖來分析Object的Monitor機制。

Monitor可以類比爲一個特殊的房間,這個房間中有一些被保護的數據,Monitor保證每次只能有一個線程能進入這個房間進行訪問被保護的數據,進入房間即爲持有Monitor,退出房間即爲釋放Monitor。

當一個線程需要訪問受保護的數據(即需要獲取對象的Monitor)時,它會首先在entry-set入口隊列中排隊(這裏並不是真正的按照排隊順序),如果沒有其他線程正在持有對象的Monitor,那麼它會和entry-set隊列和wait-set隊列中的被喚醒的其他線程進行競爭(即通過CPU調度),選出一個線程來獲取對象的Monitor,執行受保護的代碼段,執行完畢後釋放Monitor,如果已經有線程持有對象的Monitor,那麼需要等待其釋放Monitor後再進行競爭。

再說一下wait-set隊列。當一個線程擁有Monitor後,經過某些條件的判斷(比如用戶取錢發現賬戶沒錢),這個時候需要調用Object的wait方法,線程就釋放了Monitor,進入wait-set隊列,等待Object的notify方法(比如用戶向賬戶裏面存錢)。當該對象調用了notify方法或者notifyAll方法後,wait-set中的線程就會被喚醒,然後在wait-set隊列中被喚醒的線程和entry-set隊列中的線程一起通過CPU調度來競爭對象的Monitor,最終只有一個線程能獲取對象的Monitor。

需要注意的是:

當一個線程在wait-set中被喚醒後,並不一定會立刻獲取Monitor,它需要和其他線程去競爭;
如果一個線程是從wait-set隊列中喚醒後,獲取到的Monitor,它會去讀取它自己保存的PC計數器中的地址,從它調用wait方法的地方開始執行。

Monitor機制在Java中是如何實現的呢?

即通過synchronized關鍵字實現線程同步來獲取對象的Monitor。synchronized同步分爲以下兩種方式:

同步代碼塊:

synchronized(Obejct obj) {
    //同步代碼塊
    ...
}

上述代碼表示在進入同步代碼塊之前,先要去獲取obj的Monitor,如果已經被其他線程獲取了,那麼當前線程必須等待直至其他線程釋放obj的Monitor

這裏的obj可以是類.class,表示需要去獲取該類的字節碼的Monitor,獲取後,其他線程無法再去獲取到class字節碼的Monitor了,即無法訪問屬於類的同步的靜態方法了,但是對於對象的實例方法的訪問不受影響

同步方法:

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分別修飾了非靜態方法和靜態方法。

  • 非靜態方法可以理解爲,需要獲取當前對象this的Monitor,獲取後,其他需要獲取該對象的Monitor的線程會被堵塞。

  • 靜態方法可以理解爲,需要獲取該類字節碼的Monitor(因爲static方法不屬於任何對象,而是屬於類的方法),獲取後,其他需要獲取字節碼的Monitor的線程會被堵塞。

Object的notify方法和wait方法詳解

上面在講述Monitor機制的時候已經分析了notify和wait的用法,這裏具體分析下。

wait方法

wait有三個重載方法,分別如下:

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;

後面兩個傳入了時間參數(nanos表示納秒),表示如果指定時間過去還沒有其他線程調用notify或者notifyAll方法來將其喚醒,那麼該線程會自動被喚醒。

調用obj.wait方法需要注意的是,當前線程必須獲取到了obj的Monitor(The current thread must own this object's monitor),才能去調用其wait方法,即wait必須放在同步方法或同步代碼塊中。

調用的是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有兩個方法notify和notifyAll,前者只能喚醒一個正在等待這個對象的monitor的線程,具體由JVM決定,後者則會喚醒所有正在等待這個對象的monitor的線程

需要關注的點:

調用notify方法,並不意味着釋放了Monitor,必須要等同步代碼塊結束後纔會釋放Monitor。

在調用notify方法時,必須保證其他線程處於wait狀態,否則調用notify沒有任何效果,導致之後其他線程永遠處於堵塞狀態。


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.

喚醒正在此對象監視器上等待的單個線程。如果有任何線程正在等待這個對象,那麼將選擇喚醒其中的一個線程。這個選擇是任意的,由實現決定。線程通過調用其中一個等待方法來等待對象的監視器。

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.

被喚醒的線程將無法繼續前進,直到當前線程釋放該對象上的鎖。被喚醒的線程將以通常的方式與其他線程競爭,這些線程可能正在積極地對這個對象進行同步; 例如,在成爲下一個鎖定此對象的線程時,被喚醒的線程沒有任何特權或不利條件。

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.

此方法只能由此對象監視器的所有者的線程調用。線程通過以下三種方式之一成爲對象監視器的所有者:

  • 通過執行該對象的同步實例方法。
  • 通過執行同步語句的主體對對象進行同步。

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.

對於Class類型的對象,通過執行該類的同步靜態方法。
每次只有一個線程可以擁有一個對象的監視器。

Throws:

IllegalMonitorStateException – if the current thread is not the owner of this object's monitor.

See Also:

notifyAll(), wait()

小結

java中每個對象都有唯一的一個monitor,想擁有一個對象的monitor的話有以下三種方式:

1.執行該對象的同步方法

public synchronize a () {
}

2.執行該對象的同步塊

synchronize(obj) {
}

3.執行某個類的靜態同步方法

public static synchronize b(){
}

Tips:擁有monitor的是線程。

1.同時只能有一個線程可以獲取某個對象的monitor

2.一個線程通過調用某個對象的wait()方法釋放該對象的monitor並進入休眠狀態,直到其他線程獲取了被該線程釋放的monitor並調用該對象的notify()或者notifyAll()後再次競爭獲取該對象的monitor

3.只有擁有該對象monitor的線程纔可以調用該對象的notify()和notifyAll()方法

如果沒有該對象monitor的線程調用了該對象的notify()或者notifyAll()方法將會拋出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開發者社區

專注分享 Java、 Kotlin、Spring/Spring Boot、MySQL、redis、neo4j、NoSQL、Android、JavaScript、React、Node、函數式編程、編程思想、"高可用,高性能,高實時"大型分佈式系統架構設計主題。

High availability, high performance, high real-time large-scale distributed system architecture design

分佈式框架:Zookeeper、分佈式中間件框架等
分佈式存儲:GridFS、FastDFS、TFS、MemCache、redis等
分佈式數據庫:Cobar、tddl、Amoeba、Mycat
雲計算、大數據、AI算法
虛擬化、雲原生技術
分佈式計算框架:MapReduce、Hadoop、Storm、Flink等
分佈式通信機制:Dubbo、RPC調用、共享遠程數據、消息隊列等
消息隊列MQ:Kafka、MetaQ,RocketMQ
怎樣打造高可用系統:基於硬件、軟件中間件、系統架構等一些典型方案的實現:HAProxy、基於Corosync+Pacemaker的高可用集羣套件中間件系統
Mycat架構分佈式演進
大數據Join背後的難題:數據、網絡、內存和計算能力的矛盾和調和
Java分佈式系統中的高性能難題:AIO,NIO,Netty還是自己開發框架?
高性能事件派發機制:線程池模型、Disruptor模型等等。。。

合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。不積跬步,無以至千里;不積小流,無以成江河。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章