synchronized到底鎖住的是誰?

題目:利用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要按照同步代碼塊來保證線程安全,這可就加在方法「複雜」多了。有:synchronized(this){}這麼寫的,也有synchronized(Count.class){}這麼寫的,還有定義了一個private Object obj = new Object; ….synchronized(obj){}這麼寫的。此時不由在內心「W*F」。安全

synchronized你到底鎖住的是誰?併發

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

  1. 靜態方法加上關鍵字
  2. 實例方法(也就是普通方法)加上關鍵字
  3. 方法中使用同步代碼塊

前兩種方式最爲偷懶,第三種方式比前兩種性能要好。ide

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 }

synchronized到底鎖住的是誰?

 

如上圖所示,輸出結果顯然只會打印一個線程的信息,另外一個線程永遠也獲取不到這個鎖。

測試程序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 }

synchronized到底鎖住的是誰?

 

如上圖所示,顯然,兩個線程均進入到了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){...},這纔是對你代碼的改良優化。

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:787707172,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。  

相關文章
相關標籤/搜索