監視器
java中同步是通過監視器模型來實現的,JAVA中的監視器實際是一個代碼塊,這段代碼塊同一時刻只允許被一個線程執行。線程要想執行這段代碼塊的唯一方式是獲得監視器。
監視器有兩種同步方式:互斥與協作。多線程環境下線程之間如果需要共享數據,需要解決互斥訪問數據的問題,監視器可以確保監視器上的數據在同一時刻只會有一個線程在訪問。什麼時候需要協作?比如:一個線程向緩衝區寫數據,另一個線程從緩衝區讀數據,如果讀線程發現緩衝區爲空就會等待,當寫線程向緩衝區寫入數據,就會喚醒讀線程,這裏讀線程和寫線程就是一個合作關係。JVM通過Object類的wait方法來使自己等待,在調用wait方法後,該線程會釋放它持有的監視器,直到其他線程通知它纔有執行的機會。一個線程調用notify方法通知在等待的線程,這個等待的線程並不會馬上執行,而是要通知線程釋放監視器後,它重新獲取監視器纔有執行的機會。如果剛好喚醒的這個線程需要的監視器被其他線程搶佔,那麼這個線程會繼續等待。object類中的notifyAll方法可以解決這個問題,它可以喚醒所有等待的線程,總有一個線程執行。
如上圖所示,一個線程通過1號門進入Entry Set(入口區),如果在入口區沒有線程等待,那麼這個線程就會獲取監視器成爲監視器的owner,然後執行監視區域的代碼。如果在入口區中有其它線程在等待,那麼新來的線程也會和這些線程一起等待。線程在持有監視器的過程中,有兩個選擇,一個是正常執行監視器區域的代碼,釋放監視器,通過5號門退出監視器;還有可能等待某個條件的出現,於是它會通過3號門到Wait Set(等待區)休息,直到相應的條件滿足後再通過4號門進入重新獲取監視器再執行。
注意:當一個線程釋放監視器時,在入口區和等待區的等待線程都會去競爭監視器,如果入口區的線程贏了,會從2號門進入;如果等待區的線程贏了會從4號門進入。只有通過3號門才能進入等待區,在等待區中的線程只有通過4號門才能退出等待區,也就是說一個線程只有在持有監視器時才能執行wait操作,處於等待的線程只有再次獲得監視器才能退出等待狀態。
對象鎖
JVM中的一些數據,比如堆和方法區會被所有線程共享。JAVA中每個對象和類實際上都一把鎖與之相關聯,對於對象來說,監視的是這個對象的實例變量,對於類來說,監視的是類變量,如果一個對象沒有實例變量,就什麼也不監視。當虛擬機裝載類時,會創建一個Class類的實例,鎖住一個類實際上鎖住的是這個類對應的Class類的實例。對象鎖是可重入的,也就是說對一個對象或者類上的鎖可以累加。
在JAVA中有兩種監視區域:同步方法和同步塊,這兩種監視區域都和一個引入對象相關聯,當到達這個監視區域時,JVM就會鎖住這個引用對象,不論它是怎麼離開的,都會釋放這個引用對象上的鎖。JAVA程序員不能自己加對象鎖,對象鎖是JVM內部機制,只需要編寫同步方法或者同步塊即可,操作監視區域時JVM會自動幫你上鎖或者釋放鎖。
同步語句
要建立一個同步語句,只需要在相關語句加上synchronized關鍵字就可以,例如下面的incr方法,如果沒有獲得當前對象(this)的鎖,在同步塊內的語句是不會執行的,如果不是this引用,而是用另一個對象的引用,需要獲得對應對象的鎖同步塊纔會執行,如果用表達式獲得對Class對象實例的引用,就需要鎖住那個類。
- void incr() {
- synchronized (this) {
- i++;
- }
- }
以下是incr方法生成的字節碼序列:
- void incr();
- Code:
- 0: aload_0 //將this引用壓棧
- 1: dup //複製棧頂元素
- 2: astore_1 //出棧並將this引用存放在局部變量1中
- 3: monitorenter //出棧並獲取對象鎖
- 4: aload_0 //將this引用壓棧
- 5: dup //複製棧頂元素
- 6: getfield #17 //獲取i的值
- 9: iconst_1 //常數1入棧
- 10: iadd //將i+1的結果入棧
- 11: putfield #17 //將i的值存入this中
- 14: aload_1 //將this引用壓棧
- 15: monitorexit //彈出this引用釋放對象鎖
- 16: goto 22 //返回
- 19: aload_1 //19-22如果拋出,釋放對象鎖
- 20: monitorexit
- 21: athrow
- 22: return
- Exception table:
- from to target type
- 4 16 19 any
- 19 21 19 any
字節碼的第3行從棧頂中獲取對象鎖,對象鎖獲取成功後才後執行後面的add操作,第15行釋放獲取的對象鎖。注意:字節碼中出現了異常表,是用於確保加鎖的對象被釋放,即使從同步語句塊中拋出異常,也會釋放對象鎖,不然有可能導致死鎖。
同步方法
要建立同步方法,只需要在方法修飾符前加上synchronized關鍵字,類似代碼如下:
- synchronized void incr() {
- i++;
- }
- synchronized void incr();
- Code:
- 0: aload_0 //this引用壓棧
- 1: dup //複製棧頂元素
- 2: getfield #2 //獲取i的值
- 5: iconst_1 //將常量1入棧
- 6: iadd //i+1入棧
- 7: putfield #2 //將i的值存入this中
- 10: return //返回
可見,JVM並沒有使用moniterenter和moniterexit等指令,查看class文件在方法表中可以看到有0020出現,這是incr方法的訪問標誌(access flag):ACC_SYNCHRONIZED,顧名思義在說incr是一個線程同步方法。當JVM發現這是一個同步方法時,就會在這個對象或者類上獲取鎖,退出方法時會釋放這個鎖。兩段字段碼除了調用指令不同,還有一個區別是同步方法可以沒有異常表,實際上JVM隱式地做了異常處理。
優缺點:
synchronized是通過軟件(JVM)實現的,簡單易用,即使在JDK5之後有了Lock,仍然被廣泛地使用。
synchronized實際上是非公平的,新來的線程有可能立即獲得監視器,而在等待區中等候已久的線程可能再次等待,不過這種搶佔的方式可以預防飢餓。
synchronized只有鎖只與一個條件(是否獲取鎖)相關聯,不靈活,後來Condition與Lock的結合解決了這個問題。
多線程競爭一個鎖時,其餘未得到鎖的線程只能不停的嘗試獲得鎖,而不能中斷。高併發的情況下會導致性能下降。ReentrantLock的lockInterruptibly()方法可以優先考慮響應中斷。 一個線程等待時間過長,它可以中斷自己,然後ReentrantLock響應這個中斷,不再讓這個線程繼續等待。有了這個機制,使用ReentrantLock時就不會像synchronized那樣產生死鎖了。