Java併發分析—synchronized

  在計算機操做系統中,併發在宏觀上是指在同一時間段內,同時有多道程序在運行。 一個程序能夠對應一個進程或多個進程,進程有獨立的存儲空間。一個進程包含一個或多個線程。線程堆空間是共享的,棧空間是私有的。一樣,在一個進程中,宏觀上有多個線程同時運行。(微觀上在單cup系統中,同一時刻,只有一個程序在運行。)html

  基於以上原理,線程在併發運行時,對共享數據的操做存在數據同步問題。java

1.基本概念

    1.什麼樣的數據會被存儲在線程共享空間堆裏?微信

    對象,當使用new 關鍵字建立一個對象時,這個對象就被存儲在堆裏。網絡

    2.併發問題:多線程

  以一個例子來講明:併發

  建立測試類Test.java,測試類中有一個方法對變量sum加1操做,建立兩個線程tA和tB分別執行這段代碼:oracle

 1 public class Test {
 2     private int sum = 0;
 3     public void add(){
 4          try {
 5              System.out.println("線程:"+Thread.currentThread().getName()+"執行加1開始,sum當前值爲:"+sum);
 6             Thread.sleep(2000);//這兩秒錶明對其餘數據進行操做所耗費的時間
 7             sum++;
 8             System.out.println("線程:"+Thread.currentThread().getName()+"執行加1結束,sum的值爲:"+sum);
 9         } catch (InterruptedException e) {
10             // TODO Auto-generated catch block
11             e.printStackTrace();
12         }
13     }
14     public static void main(String [] args){
15         final Test test = new Test();
16         Thread tA = new Thread(new Runnable() {
17             
18             @Override
19             public void run() {
20                 // TODO Auto-generated method stub
21                 test.add();
22             }
23         });
24         Thread tB = new Thread(new Runnable() {
25             
26             @Override
27             public void run() {
28                 // TODO Auto-generated method stub
29                 test.add();
30             }
31         });
32         tA.start();
33         tB.start();
34     }
35 }
View Code

  運行結果:app

線程:Thread-1執行加1開始,sum當前值爲:0
線程:Thread-0執行加1開始,sum當前值爲:0
線程:Thread-1執行加1結束,sum的值爲:1
線程:Thread-0執行加1結束,sum的值爲:1

  從以上數據來看,這個結果明顯不對,兩次加操做,最後的值應是2。異步

  緣由分析:兩個線程前後執行add方法時,拿到的數據都是0,再對共享數據加1,最後結果都是1。jvm

2.解決線程併發問題-synchronized

2.1 使用位置

  1.普通同步方法,鎖加在當前實例對象上。

  2.靜態方法,鎖加載當前類對象上。

  3.同步方法塊,鎖住的是synchronized (xxx)括號裏的對象。

  解析: 從1.0開始,java中的每個對象都有一個內部鎖(這是加鎖的基礎)。

  對於普通同步方法,若是一個方法使用synchronized 關鍵字聲明,那麼對象鎖將保護整個方法,做用的對象是調用這個方法的對象。當某一個線程運行到該方法時,須要檢查有沒有其餘線程正在使用這個方法,有的話須要等待那個線程運行完這個方法後在運行,沒有的話,須要鎖定這個方法,而後運行。

  對於靜態方法聲明爲synchronized ,若是調用這種方法,由於靜態方法是類方法,其做用的範圍是整個方法,做用的對象是這個類的全部對象,該方法得到到相關的類對的內部鎖,所以,沒有其餘線程能夠調用同一個類的同步靜態方法。

  對於同步代碼塊,被修飾的代碼塊稱爲同步語句塊,其做用的範圍是大括號{}括起來的代碼,做用的對象是調用這個代碼塊的對象,也就是括號裏面的對象,synchronized 括號裏能夠反射獲取類的對象,例如本示例中能夠寫成  synchronized (this),也能夠寫成Test.class

2.2 實現原理

2.2.1. 普通同步方法:

  以上面代碼爲例,只須要給普通方法上加上synchronized 關鍵字,其餘代碼不變,只貼改變了的代碼.

 1 public synchronized void add(){
 2          try {
 3              System.out.println("線程:"+Thread.currentThread().getName()+"執行加1開始,sum當前值爲:"+sum);
 4             Thread.sleep(2000);//這兩秒錶明對其餘數據進行操做所耗費的時間
 5             sum++;
 6             System.out.println("線程:"+Thread.currentThread().getName()+"執行加1結束,sum的值爲:"+sum);
 7         } catch (InterruptedException e) {
 8             // TODO Auto-generated catch block
 9             e.printStackTrace();
10         }
11     }
View Code

  運行結果

1 線程:Thread-0執行加1開始,sum當前值爲:0
2 線程:Thread-0執行加1結束,sum的值爲:1
3 線程:Thread-1執行加1開始,sum當前值爲:1
4 線程:Thread-1執行加1結束,sum的值爲:2

  從以上數據能夠看出,做用的對象是調用這個方法的對象,當第一個線程執行完此方法,第二個線程纔開始執行。

  在  ..\bin\com\test 目錄下找到類對應的class文件,個人是Test.class,   使用  Javap  -v Test.class  命令查看字節碼信息,以下:

        

  經過上面的截圖能夠看到,在add()方法上加了一個 ACC_SYNCHRONIZED  標識,JVM在解析的時候,根據這個標識實現方法同步。

2.2.2.靜態方法

  先看問題,將上面的代碼改造爲兩個對象,代碼以下:

 1 package com.test;
 2 
 3 public class Main {
 4     public static int i = 0;
 5     public static  void add() {
 6         try {
 7             System.out.println("線程"+Thread.currentThread().getName()+"調用add()方法前,i的值爲:"+i);
 8             Thread.sleep(2000);
 9             i=i+1;
10             System.out.println("線程"+Thread.currentThread().getName()+"調用add()方法後,i的值爲:"+i);
11         }catch (InterruptedException e){
12             e.printStackTrace();
13         }
14     }
15     public static void main(String[] args) {
16 
17         final Main min1  = new Main(); //對象1
18         final Main min2 = new Main();  //對象2
19         Thread ta = new Thread(new Runnable() {
20             public void run() {
21                 min1.add();
22             }
23         });
24         Thread tb = new Thread(new Runnable() {
25             public void run() {
26                 min2.add();
27             }
28         });
29         ta.start();
30         tb.start();
31         
32     }
33 }
View Code

   運行結果:

1 線程Thread-0調用add()方法前,i的值爲:0
2 線程Thread-1調用add()方法前,i的值爲:0
3 線程Thread-0調用add()方法後,i的值爲:1
4 線程Thread-1調用add()方法後,i的值爲:2

  從運行結果能夠看出,兩個線程是交叉執行的,可是結果倒是正確的,沒有什麼問題。可是,若是將線程增長到五個再看一下:

 1 package com.test;
 2 
 3 public class Main {
 4     public static int i = 0;
 5     public static  void add() {
 6         try {
 7             System.out.println("線程"+Thread.currentThread().getName()+"調用add()方法前,i的值爲:"+i);
 8             Thread.sleep(2000);
 9             i=i+1;
10             System.out.println("線程"+Thread.currentThread().getName()+"調用add()方法後,i的值爲:"+i);
11         }catch (InterruptedException e){
12             e.printStackTrace();
13         }
14     }
15     public static void main(String[] args) {
16 
17         final Main min1  = new Main(); //對象1
18         final Main min2 = new Main();  //對象2
19         final Main min3  = new Main(); //對象3
20         final Main min4 = new Main();  //對象4
21         final Main min5  = new Main(); //對象5
22         Thread ta = new Thread(new Runnable() {
23             public void run() {
24                 min1.add();
25             }
26         });
27         Thread tb = new Thread(new Runnable() {
28             public void run() {
29                 min2.add();
30             }
31         });
32         
33         Thread tc = new Thread(new Runnable() {
34             public void run() {
35                 min3.add();
36             }
37         });
38         Thread td = new Thread(new Runnable() {
39             public void run() {
40                 min4.add();
41             }
42         });
43         Thread te = new Thread(new Runnable() {
44             public void run() {
45                 min5.add();
46             }
47         });
48         tc.start();
49         td.start();
50         te.start();
51         ta.start();
52         tb.start();
53         
54     }
55 }
View Code

  運行結果:

 1 線程Thread-3調用add()方法前,i的值爲:0
 2 線程Thread-4調用add()方法前,i的值爲:0
 3 線程Thread-2調用add()方法前,i的值爲:0
 4 線程Thread-0調用add()方法前,i的值爲:0
 5 線程Thread-1調用add()方法前,i的值爲:0
 6 線程Thread-0調用add()方法後,i的值爲:1
 7 線程Thread-1調用add()方法後,i的值爲:3
 8 線程Thread-3調用add()方法後,i的值爲:2
 9 線程Thread-2調用add()方法後,i的值爲:5
10 線程Thread-4調用add()方法後,i的值爲:4

  一眼就看出有問題了,緣由再也不討論。再看給add()方法加鎖後的狀況:

 1 public static synchronized void add() {
 2         try {
 3             System.out.println("線程"+Thread.currentThread().getName()+"調用add()方法前,i的值爲:"+i);
 4             Thread.sleep(2000);
 5             i=i+1;
 6             System.out.println("線程"+Thread.currentThread().getName()+"調用add()方法後,i的值爲:"+i);
 7         }catch (InterruptedException e){
 8             e.printStackTrace();
 9         }
10     }
View Code

  運行結果:

 1 線程Thread-2調用add()方法前,i的值爲:0
 2 線程Thread-2調用add()方法後,i的值爲:1
 3 線程Thread-1調用add()方法前,i的值爲:1
 4 線程Thread-1調用add()方法後,i的值爲:2
 5 線程Thread-0調用add()方法前,i的值爲:2
 6 線程Thread-0調用add()方法後,i的值爲:3
 7 線程Thread-4調用add()方法前,i的值爲:3
 8 線程Thread-4調用add()方法後,i的值爲:4
 9 線程Thread-3調用add()方法前,i的值爲:4
10 線程Thread-3調用add()方法後,i的值爲:5

   這個結果就順眼多了,再看字節碼狀況:

        

  通過查看發現,這個加鎖方式和普通方法加鎖方式同樣,都是加了一個標誌。

2.2.3.同步方法塊

  將add()方法改造以下:

 1 public void add(){
 2          synchronized (this) {
 3               try {
 4                  System.out.println("線程:"+Thread.currentThread().getName()+"執行加1開始,sum當前值爲:"+sum);
 5                 Thread.sleep(2000);   //這兩秒錶明對其餘數據進行操做所耗費的時間
 6                 sum++;
 7                  System.out.println("線程:"+Thread.currentThread().getName()+"執行加1結束,sum的值爲:"+sum);
 8             } catch (InterruptedException e) {
 9                 // TODO Auto-generated catch block
10                 e.printStackTrace();
11             }
12               
13         }
14     }
View Code

  運行結果:    

1 線程:Thread-1執行加1開始,sum當前值爲:0
2 線程:Thread-1執行加1結束,sum的值爲:1
3 線程:Thread-0執行加1開始,sum當前值爲:1
4 線程:Thread-0執行加1結束,sum的值爲:2

  以上運行結果正常,查看字節碼信息:

  爲了查看信息量小,將同步代碼塊內的代碼屏蔽掉,而後編譯,查看字節碼信息以下:

        

        同步方法塊和以上兩種就不同了,是經過  monitorenter  和 monitorexit   給對象 this  也就是Test類的對象加鎖和解鎖。

        若是給同步代碼塊內添加任意可執行代碼,狀況就變了,好比加一句打印語句(不上圖,自行想象),字節碼信息以下:

       

  竟然出現兩條monitorexit ,可是隻有一個monitorenter,這就是鎖的重入性,什麼意思呢?對於同一個類的對象,線程在執行一個任務時,會獲取一次鎖,當執行完會釋放鎖,若是這個線程還要繼續執行這個對象的其餘任務,是不須要從新獲取鎖的,但執行完任務就要釋放鎖,顧名思義,鎖的重入性。

  綜上,synchronized 加鎖的方法就是使用ACC_SYNCHRONIZED  和 monitorentermonitorexit 實現的,那麼,接下來,就研究研究這三個詞是什麼。

2.2.4 synchronized 原理   

   https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10 中的解釋是這樣的:

  同步方法在運行時常量池的method_info結構中經過ACC_SYNCHRONIZED標誌區分,該標誌由方法調用指令檢查。當調用設置了ACC_SYNCHRONIZED的方法時,執行線程進入監視器,調用方法自己,並退出監視器,不管方法調用是正常仍是忽然完成。在執行線程擁有監視器期間,沒有其餘線程能夠輸入它。若是在調用synchronized方法期間拋出異常而且synchronized方法不處理異常,則在異步從同步方法中從新拋出以前,將自動退出該方法的監視器。

  這裏有個關鍵詞:監視器。監視器是什麼呢? 監視器又名monitor。每一個對象都是一個監視器鎖,當對象監視器鎖被佔用時,對象就是鎖定狀態,其餘線程不能對其操做,當佔用被解除時,其餘線程就能夠獲取此對象。

  那麼上面三個詞是怎麼工做的呢?

  a.monitorentermonitorexit 

  monitorenter 和 monitorexit 是線程執行同步代碼塊時執行的指令,當線程執行同步代碼塊時會佔用監視器,執行monitorexit 指令會解除監視器。

  b.ACC_SYNCHRONIZED

  當JVM調用方法時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。

  因此,兩種方法本質沒有區別。

  要明白監視器怎麼工做的,就得研究研究對象。

  對象在內存中存儲的佈局能夠分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。對象頭主要包括兩部分(對象其餘信息在此不作研究)   markword  和  klass ,與鎖有關的信息就存儲子在markword中 ,以下圖:

                          

  在64位虛擬機下,Mark Word是64bit大小的,其存儲結構以下:

             

                                                                                 圖片來自網絡,若有雷同,純屬巧合。    

  從圖片能夠看出,對象頭的後兩位存儲了鎖的標誌。初始狀態是01,標識位加鎖。偏向鎖的標誌位存儲的是佔用當前對象的線程的ID。

2.2.5 synchronized 優化

  從上面的例子中,能夠體會到,當有多個線程訪問同步代碼塊時,若是每一個線程執行幾秒,那麼將會很消耗時間,故而,須要對鎖進行優化。

  高效 併發是從JDK1.5到JDK1.6的一個重要改進,目前的優化技術有 適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖 等 這些技術是爲了在線程之間更高效地共享數據,以解決競爭問題,從而提升程序的執行效率。

  自旋鎖與自適應鎖

  若是物理機器上有一個以上的處理器,能讓兩個或兩個以上的線程同時並行執行,就可讓後面請求鎖的那個線程稍微等待一下,但不放棄處理器的執行時間,看看持有線程的鎖是否很快就會釋放鎖,爲了讓線程等待,只須要讓線程執行一個忙循環即自旋,這就是自旋鎖。

  自旋鎖在JDK 1.4.2中引入,默認關閉,可使用-XX:+UseSpinning開啓,在JDK1.6中默認開啓。默認次數能夠經過參數-XX:PreBlockSpin來調整。

  若是所被佔用的時間很短,自旋等待的效果就會很是的好,反之,自旋的線程只會白白得消耗處理器資源,而不會作任何有用的工做,反而帶來性能上的浪費,所以,自選鎖等待的時間必需要有必定的限制,若是自旋鎖超過了限定的次數仍然沒有成功得到鎖,就應當使用傳統的方式掛起鎖了,默認次數是10。爲了解決這個問題,引入自適應鎖,JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定,線程若是自旋成功了,那麼下次自旋的次數會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之,若是對於某個鎖,不多有自旋可以成功,那麼在之後要或者這個鎖的時候自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。

  鎖消除

    鎖消除是Java虛擬機在JIT編譯時,經過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,經過鎖消除,能夠節省毫無心義的請求鎖時間。

  鎖粗化

    若是一系列的連續操做都是對同一對象反覆加鎖和解鎖,甚至加鎖操做時出如今循環體中,那便是沒有線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗,若是虛擬機探測到有這樣一串零碎的操做都對同一個對象加鎖,將會把加鎖同步範圍擴展到整個操做序列的外部,就擴展到第一個append()操做以前直至最後一個append()操做以後,這樣只須要加鎖一次就好。

  輕量級鎖

  輕量級鎖是JDK1,6中加入的新型鎖機制,它是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。

  偏向鎖

  在JVM1.6中引入了偏向鎖,偏向鎖主要解決無競爭下的鎖性能問題,首先咱們看下無競爭下鎖存在什麼問題:
  如今幾乎全部的鎖都是可重入的,也即已經得到鎖的線程能夠屢次鎖住/解鎖監視對象,按照以前的HotSpot設計,每次加鎖/解鎖都會涉及到一些CAS操做(好比對等待隊列的CAS操做),CAS操做會延遲本地調用,所以偏向鎖的想法是一旦線程第一次得到了監視對象,以後讓監視對象「偏向」這個線程,以後的屢次調用則能夠避免CAS操做,說白了就是置個變量,若是發現爲true則無需再走各類加鎖/解鎖流程。

2.2.6 內部鎖條件的侷限性:

  (1)不能中斷一個正在試圖得到鎖的線程

  (2)試圖得到鎖時不能設定超時

  (3)每一個鎖僅有單一的條件,多是不夠的。

             歡迎掃碼關注個人微信公衆號,或者微信公衆號直接搜索Java傳奇,不定時更新一些學習筆記!

                                                     

相關文章
相關標籤/搜索