synchronized到底鎖住的是誰?

OKevin發表於2019-06-14

本文程式碼倉庫:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sync

先來一道校招級併發程式設計筆試題

題目:利用5個執行緒併發執行,num數字累計計數到10000,並列印。

 1 /**
 2 * Description:
 3 * 利用5個執行緒併發執行,num數字累加計數到10000,並列印。
 4 * 2019-06-13
 5 * Created with OKevin.
 6 */
 7 public class Count {
 8    private int num = 0;
 9 
10    public static void main(String[] args) throws InterruptedException {
11        Count count = new Count();
12 
13        Thread thread1 = new Thread(count.new MyThread());
14        Thread thread2 = new Thread(count.new MyThread());
15        Thread thread3 = new Thread(count.new MyThread());
16        Thread thread4 = new Thread(count.new MyThread());
17        Thread thread5 = new Thread(count.new MyThread());
18        thread1.start();
19        thread2.start();
20        thread3.start();
21        thread4.start();
22        thread5.start();
23        thread1.join();
24        thread2.join();
25        thread3.join();
26        thread4.join();
27        thread5.join();
28 
29        System.out.println(count.num);
30 
31    }
32 
33    private synchronized void increse() {
34        for (int i = 0; i < 2000; i++) {
35            num++;
36        }
37    }
38 
39    class MyThread implements Runnable {
40        @Override
41        public void run() {
42            increse();
43        }
44    }
45 }

這道校招級的併發程式設計面試題,題目不難,方法簡單。其中涉及一個核心知識點——synchronized(當然這題的解法有很多),這也是本文想要弄清的主題。

synchronized被大大小小的程式設計師廣泛使用,有的程式設計師偷懶,在要求保證執行緒安全時,不加思索的就在方法前加入了synchronized關鍵字(例如我剛才那道校招級大題)。偷懶歸偷懶,CodeReview總是要進行的,面對同事的“指責”,要求優化這個方法,將synchronized使用同步程式碼塊的方式提高效率。

 

synchronized要按照同步程式碼塊來保證執行緒安全,這可就加在方法“複雜”多了。有:synchronized(this){}這麼寫的,也有synchronized(Count.class){}這麼寫的,還有定義了一個private Object obj = new Object; ….synchronized(obj){}這麼寫的。此時不禁在心裡“W*F”。

synchronized你到底鎖住的是誰?

synchronized從語法的維度一共有3個用法:

  1. 靜態方法加上關鍵字

  2. 例項方法(也就是普通方法)加上關鍵字

  3. 方法中使用同步程式碼塊

前兩種方式最為偷懶,第三種方式比前兩種效能要好。

synchronized從鎖的是誰的維度一共有兩種情況:

  1. 鎖住類

  2. 鎖住物件例項

我們還是從直觀的語法結構上來講述synchronized。

1)靜態方法上的鎖

靜態方法是屬於“類”,不屬於某個例項,是所有物件例項所共享的方法。也就是說如果在靜態方法上加入synchronized,那麼它獲取的就是這個類的鎖,鎖住的就是這個類

2)例項方法(普通方法)上的鎖

例項方法並不是類所獨有的,每個物件例項獨立擁有它,它並不被物件例項所共享。這也比較能推出,在例項方法上加入synchronized,那麼它獲取的就是這個累的鎖,鎖住的就是這個物件例項

那鎖住類還是鎖住物件例項,這跟我執行緒安全關係大嗎?大,差之毫釐謬以千里的大。為了更好的理解鎖住類還是鎖住物件例項,在進入“3)方法中使用同步程式碼塊”前,先直觀的感受下這兩者的區別。

對例項方法(普通方法)上加關鍵字鎖住物件例項鎖的解釋

首先定義一個Demo類,其中的例項方法加上了synchronized關鍵字,按照所述也就是說鎖住的物件例項。

 1 /**
 2 * Description:
 3 * 死迴圈,目的是兩個執行緒搶佔一個鎖時,只要其中一個執行緒獲取,另一個執行緒就會一直阻塞
 4 * 2019-06-13
 5 * Created with OKevin.
 6 */
 7 public class Demo {
 8 
 9    public synchronized void demo() {
10        while (true) {   //synchronized方法內部是一個死迴圈,一旦一個執行緒持有過後就不會釋放這個鎖
11            System.out.println(Thread.currentThread());
12        }
13    }
14 }

可以看到在demo方法中定義了一個死迴圈,一旦一個執行緒持有這個鎖後其他執行緒就不可能獲取這個鎖。結合上述synchronized修飾例項方法鎖住的是物件例項,如果兩個執行緒針對的是一個物件例項,那麼其中一個執行緒必然不可能獲取這個鎖;如果兩個執行緒針對的是兩個物件例項,那麼這兩個執行緒不相關均能獲取這個鎖。

自定義執行緒,呼叫demo方法。

 1 /**
 2 * Description:
 3 * 自定義執行緒
 4 * 2019-06-13
 5 * Created with OKevin.
 6 */
 7 public class MyThread implements Runnable {
 8    private Demo demo;
 9 
10    public MyThread(Demo demo) {
11        this.demo = demo;
12    }
13 
14    @Override
15    public void run() {
16        demo.demo();
17    }
18 }

測試程式1:兩個執行緒搶佔一個物件例項的鎖

 1 /**
 2 * Description:
 3 * 兩個執行緒搶佔一個物件例項的鎖
 4 * 2019-06-13
 5 * Created with OKevin.
 6 */
 7 public class Main1 {
 8    public static void main(String[] args) {
 9        Demo demo = new Demo();
10        Thread thread1 = new Thread(new MyThread(demo));
11        Thread thread2 = new Thread(new MyThread(demo));
12        thread1.start();
13        thread2.start();
14    }
15 }

 如上圖所示,輸出結果顯然只會列印一個執行緒的資訊,另一個執行緒永遠也獲取不到這個鎖。

測試程式2:兩個執行緒分別搶佔兩個物件例項的鎖

 1 /**
 2 * Description:
 3 * 兩個執行緒分別搶佔兩個物件例項的鎖
 4 * 2019-06-13
 5 * Created with OKevin.
 6 */
 7 public class Main2 {
 8    public static void main(String[] args) {
 9        Demo demo1 = new Demo();
10        Demo demo2 = new Demo();
11        Thread thread1 = new Thread(new MyThread(demo1));
12        Thread thread2 = new Thread(new MyThread(demo2));
13        thread1.start();
14        thread2.start();
15    }
16 }

如上圖所示,顯然,兩個執行緒均進入到了demo方法,也就是均獲取到了鎖,證明,兩個執行緒搶佔的就不是同一個鎖,這就是synchronized修飾例項方法時,鎖住的是物件例項的解釋。

對靜態方法上加關鍵字鎖住類鎖的解釋

靜態方法是類所有物件例項所共享的,無論定義多少個例項,是要是靜態方法上的鎖,它至始至終只有1個。將上面的程式Demo中的方法加上static,無論使用“測試程式1”還是“測試程式2”,均只有一個執行緒可以搶佔到鎖,另一個執行緒仍然是永遠無法獲取到鎖。

 

讓我們重新回到從語法結構上解釋synchronized。

3)方法中使用同步程式碼塊

程式的改良優化需要建立在有堅實的基礎,如果在不瞭解其內部機制,改良也僅僅是“形式主義”。

結合開始CodeReview的例子:

你的同事在CodeReview時,要求你將例項方法上的synchronized,改為效率更高的同步程式碼塊方式。在你不清楚同步程式碼的用法時,網上搜到了一段synchronized(this){}程式碼,複製下來發現也能用,此時你以為你改良優化了程式碼。但實際上,你可能只是做了一點形式主義上的優化。


為什麼這麼說?這需要清楚地認識同步程式碼塊到底應該怎麼用。

3.1)synchronized(this){...}

this關鍵字所代表的意思是該物件例項,換句話說,這種用法synchronized鎖住的仍然是物件例項,他和public synchronized void demo(){}可以說僅僅是做了語法上的改變。 

 1 /**
 2 * 2019-06-13
 3 * Created with OKevin.
 4 **/
 5 public class Demo {
 6   
 7    public synchronized void demo1() {
 8        while (true) {  //死迴圈目的是為了讓執行緒一直持有該鎖
 9            System.out.println(Thread.currentThread());
10        }
11    }
12 
13    public synchronized void demo2() {
14        while (true) {
15            System.out.println(Thread.currentThread());
16        }
17    }
18 }

改為以下方式: 

 1 /**
 2 * Description:
 3 * synchronized同步程式碼塊對本例項加鎖(this)
 4 * 假設demo1與demo2方法不相關,此時兩個執行緒對同一個物件例項分別呼叫demo1與demo2,只要其中一個執行緒獲取到了鎖即執行了demo1或者demo2,此時另一個執行緒會永遠處於阻塞狀態
 5 * 2019-06-13
 6 * Created with OKevin.
 7 */
 8 public class Demo {
 9 
10    public void demo1() {
11        synchronized (this) {
12            while (true) {  //死迴圈目的是為了讓執行緒一直持有該鎖
13                System.out.println(Thread.currentThread());
14            }
15        }
16    }
17 
18    public void demo2() {
19        synchronized (this) {
20            while (true) {
21                System.out.println(Thread.currentThread());
22            }
23        }
24    }
25 }

也許後者在JVM中可能會做一些特殊的優化,但從程式碼分析上來講,兩者並沒有做到很大的優化,執行緒1執行demo1,執行緒2執行demo2,由於兩個方法均是搶佔物件例項的鎖,只要有一個執行緒獲取到鎖,另外一個執行緒只能阻塞等待,即使兩個方法不相關。

3.2)private Object obj = new Object();    synchronized(obj){...}

 1 /**
 2 * Description:
 3 * synchronized同步程式碼塊對物件內部的例項加鎖
 4 * 假設demo1與demo2方法不相關,此時兩個執行緒對同一個物件例項分別呼叫demo1與demo2,均能獲取各自的鎖
 5 * 2019-06-13
 6 * Created with OKevin.
 7 */
 8 public class Demo {
 9    private Object lock1 = new Object();
10    private Object lock2 = new Object();
11 
12    public void demo1() {
13        synchronized (lock1) {
14            while (true) {  //死迴圈目的是為了讓執行緒一直持有該鎖
15                System.out.println(Thread.currentThread());
16            }
17        }
18    }
19 
20    public void demo2() {
21        synchronized (lock2) {
22            while (true) {
23                System.out.println(Thread.currentThread());
24            }
25        }
26    }
27 }

經過上面的分析,看到這裡,你可能會開始懂了,可以看到demo1方法中的同步程式碼塊鎖住的是lock1物件例項,demo2方法中的同步程式碼塊鎖住的是lock2物件例項。如果執行緒1執行demo1,執行緒2執行demo2,由於兩個方法搶佔的是不同的物件例項鎖,也就是說兩個執行緒均能獲取到鎖執行各自的方法(當然前提是兩個方法互不相關,才不會出現邏輯錯誤)。

 

3.3)synchronized(Demo.class){...}

這種形式等同於搶佔獲取類鎖,這種方式,同樣和3.1一樣,收效甚微。

 

所以CodeReivew後的程式碼應該是3.2) private Object obj = new Object();    synchronized(obj){...},這才是對你程式碼的改良優化。

 

本文程式碼倉庫:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sync

關注公眾號:coderbuff,下期預告:synchronized憑什麼鎖得住?

 

 

這是一個能給程式設計師加buff的公眾號 (CoderBuff)

 

相關文章