synchronized關鍵字

監視器

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對象實例的引用,就需要鎖住那個類。

[java] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. void incr() {  
  2.     synchronized (this) {  
  3.         i++;  
  4.     }  
  5. }  

以下是incr方法生成的字節碼序列:

[java] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. void incr();  
  2.    Code:  
  3.       0: aload_0            //將this引用壓棧  
  4.       1: dup                //複製棧頂元素  
  5.       2: astore_1           //出棧並將this引用存放在局部變量1中  
  6.       3: monitorenter           //出棧並獲取對象鎖  
  7.       4: aload_0            //將this引用壓棧  
  8.       5: dup                //複製棧頂元素  
  9.       6: getfield      #17             //獲取i的值  
  10.       9: iconst_1           //常數1入棧  
  11.      10: iadd               //將i+1的結果入棧  
  12.      11: putfield      #17             //將i的值存入this中  
  13.      14: aload_1            //將this引用壓棧  
  14.      15: monitorexit            //彈出this引用釋放對象鎖  
  15.      16goto          22       //返回  
  16.      19: aload_1            //19-22如果拋出,釋放對象鎖  
  17.      20: monitorexit     
  18.      21: athrow          
  19.      22return          
  20.    Exception table:  
  21.       from    to  target type  
  22.           4    16    19   any  
  23.          19    21    19   any  

字節碼的第3行從棧頂中獲取對象鎖,對象鎖獲取成功後才後執行後面的add操作,第15行釋放獲取的對象鎖。注意:字節碼中出現了異常表,是用於確保加鎖的對象被釋放,即使從同步語句塊中拋出異常,也會釋放對象鎖,不然有可能導致死鎖。

同步方法

要建立同步方法,只需要在方法修飾符前加上synchronized關鍵字,類似代碼如下:

[java] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. synchronized void incr() {  
  2.     i++;  
  3. }  
生成的字節碼序列如下:

[java] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. synchronized void incr();  
  2.     Code:  
  3.        0: aload_0           //this引用壓棧  
  4.        1: dup               //複製棧頂元素  
  5.        2: getfield      #2              //獲取i的值  
  6.        5: iconst_1          //將常量1入棧  
  7.        6: iadd              //i+1入棧  
  8.        7: putfield      #2              //將i的值存入this中  
  9.       10return            //返回  

可見,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那樣產生死鎖了。

發佈了29 篇原創文章 · 獲贊 19 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章