Java synchronized的原理解析

開始


 

類有一個特性叫封裝,若是一個類,全部的field都是private的,並且沒有任何的method,那麼這個類就像是四面圍牆+天羅地網,沒有門。看起來就是一個封閉的箱子,外面的進不來,裏面的出不去,通常來講,這樣的類是沒用的。多線程


如今爲這個類定義一個public的method,這個method可以修改這個類的field,至關於爲這個箱子開了一個門。門有了,而後訪問者就有了,當一個時間段,有多個訪問者進來,就可能會發生併發問題。
 
併發問題是個什麼問題?最經典的例子就是轉帳,一個訪問者從帳戶A扣取一部分金額,加到帳戶B上。在A帳戶扣取以後,B帳戶轉入以前,數據處於不一致的狀態,另外一個訪問者若是在這個時候訪問B帳戶,獲取的數據就是有問題的。這就是併發問題,致使這個問題的出現基於2個條件:1.訪問者的操做致使數據在一段時間內是不一致的;2.能夠有多個訪問者同時操做。若是可以破壞其中一個條件,就能夠解決併發問題了。咱們的關注點是在第2個條件上。
 
回到那個箱子,回到那個門。咱們設想爲這個門加一把鎖,一個訪問者進了這個門,就上鎖,期間其餘訪問者不能再進來;等進去的訪問者出來,鎖打開,容許另外一個訪問者進去。

1. 給一個代碼塊上鎖

synchronized能夠上鎖、解鎖。可是它自己並非鎖,它使用的鎖來自於一個對象: 任何對象實例都有一把內部鎖,只有一把synchronized不只僅能夠對整個method上鎖,還能夠對method內的某個代碼塊上鎖。
好比下面這種用法:
synchronized(obj){
    // some code...
}

這個用法就是使用了obj的鎖,來鎖定一個代碼塊。併發

對整個方法上鎖,如:
1 publicsynchronizedvoid aMethod(){
2     // some code...
3 }

這個時候它使用的是當前實例this的鎖,至關於下面的模式:性能

publicvoid aMethod(){
    synchronized(this){
        // some code...
    }
}

2. 兩個代碼塊的互斥

一個代碼塊,被上了鎖,就沒法同時接納多個線程的訪問。若是是2個不一樣的代碼塊,都被上了鎖,它們之間是否會有影響呢?請看下面的代碼:
 1 class SyncData {
 2     public void do1() {
 3         synchronized(this) {
 4             for (int i=0; i < 4; i++) {
 5                 System.out.println(Thread.currentThread().getName() + "-do1-" + i);
 6                 try{
 7                     Thread.sleep(1000);
 8                 }catch(InterruptedException e) {
 9                     e.printStackTrace();
10                 }
11             }
12         }
13         
14     }
15     
16     public void do2() {
17         synchronized(this) {
18             for (int i=0; i < 4; i++) {
19                 System.out.println(Thread.currentThread().getName() + "-do2-" + i);
20                 try{
21                     Thread.sleep(1000);
22                 }catch(InterruptedException e) {
23                     e.printStackTrace();
24                 }
25             }
26         }
27     }
28 }

建立1個SyncData的實例,開啓2個線程,一個線程調用實例的do1方法,另外一個線程調用實例的do2方法,你會看到他們之間是互斥的——即便2個線程訪問的是實例的不一樣的方法,依然不能同時訪問。由於決定是否能夠同時訪問的再也不是門,而是鎖。只要使用的是相同的對象鎖,就會互斥訪問this

上文中關於門的比喻已經不合適了,由於在代碼中你能夠發現兩個門(do一、do2)使用了同一把鎖(this),而這和咱們的常識經驗是相違背的,下文也不會再出現「門」。

3. 鎖的識別

可使用任何對象的鎖,好比你能夠專門建立一個對象,只提供鎖的功能:
 1 class SyncData {
 2     private Object lock = new byte[0];
 3     
 4     public void do1() {
 5         synchronized(lock) {
 6             for (int i=0; i < 4; i++) {
 7                 System.out.println(Thread.currentThread().getName() + "-do1-" + i);
 8                 try{
 9                     Thread.sleep(1000);
10                 }catch(InterruptedException e) {
11                     e.printStackTrace();
12                 }
13             }
14         }
15     }
16 }

思考下面的代碼是否能起到互斥訪問的做用:spa

 1 class SyncData {
 2     public void do1() {
 3         Object lock = new byte[0];
 4         synchronized(lock) {
 5             for (int i=0; i < 4; i++) {
 6                 System.out.println(Thread.currentThread().getName() + "-do1-" + i);
 7                 try{
 8                     Thread.sleep(1000);
 9                 }catch(InterruptedException e) {
10                     e.printStackTrace();
11                 }
12             }
13         }
14     }
15 }

這個是不能起到互斥做用的,由於每一次調用,局部變量lock都是不一樣的實例。也就是說,synchronized使用的鎖老是變化的。因此咱們再補充一點:只有使用相同的對象鎖,才能互斥訪問。因此識別所使用的鎖,是很重要的。.net

 
下面再看一段代碼:
 1 class SyncData {
 2     public void do1() {
 3         synchronized(this) {
 4             for (int i=0; i < 4; i++) {
 5                 System.out.println(Thread.currentThread().getName() + "-do1-" + i);
 6                 try{
 7                     Thread.sleep(1000);
 8                 }catch(InterruptedException e) {
 9                     e.printStackTrace();
10                 }
11             }
12         }
13         
14     }
15 }

建立2個實例,分別交給2個線程中的1個去訪問,能互斥嗎?線程

不能夠,由於每個實例使用的都是自身的鎖,相互之間是不一樣的鎖,因此不能互斥。若是把代碼改爲這樣呢:
class SyncData {
    public void do1() {
        synchronized(this.getClass()) {
            for (int i=0; i < 4; i++) {
                System.out.println(Thread.currentThread().getName() + "-do1-" + i);
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
    }
}

能夠互斥,無論一個類有多少個實例,它們調用getClass()返回的結果都是同一個實例。設計

討論這個問題,是由於能夠在static的method上使用synchronized,而其本質,就是使用了上面那種實例的鎖,因此不一樣的synchronized static方法之間,也是互斥的。

總結


總結一下咱們的結論:
  1. 任何對象實例都有一把內部鎖,只有一把。
  2. 相同的對象鎖是互斥訪問的充要條件。
這2個結論已經夠了,重要的是識別使用的對象的鎖是否是相同的。
 
多線程設計,考慮同步問題,我有幾點想法:
  1. 一個類的實例,可能被多個線程併發訪問,才考慮同步控制。
  2. 在1的前提下,只有會致使數據狀態出現一段時間的不一致,相關的代碼片斷才須要同步控制。
  3. 在2的前提下,只有兩塊代碼會相互干擾時,才必須使用同一把對象鎖,來實現互斥;若是相互之間沒有影響,建議使用不一樣的對象鎖,以保持併發性能。
固然,在判斷「數據狀態是否會不一致」、「兩塊代碼是否有干擾」的時候,是比較困難的,因此再補充2點:
  1. 在不能確認數據狀態是否會不一致的狀況下,按照會不一致的狀況考慮
  2. 在不能確認兩塊代碼是否有干擾的狀況下,按照會有干擾的狀況考慮
咱們的討論到此結束。
 

參考


  1. Java中Synchronized的用法介紹了使用synchronized的幾種方式,以及相互的區別,寫的很好,建議也看一下,相互印證。
相關文章
相關標籤/搜索