Java 多執行緒乾貨系列—(二)synchronized | 掘金技術徵文

嘟嘟MD發表於2017-05-04

原本地址:Java多執行緒乾貨系列—(二)synchronized
部落格地址:tengj.top/

前言

本篇主要介紹Java多執行緒中的同步,也就是如何在Java語言中寫出執行緒安全的程式,如何在Java語言中解決非執行緒安全的相關問題。沒錯就是使用synchronized。

正文

如何解決執行緒安全問題?

那麼一般來說,是如何解決執行緒安全問題的呢?

基本上所有的併發模式在解決執行緒安全問題時,都採用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個執行緒訪問臨界資源,也稱作同步互斥訪問。

通常來說,是在訪問臨界資源的程式碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他執行緒繼續訪問。

在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。

本文主要講述synchronized的使用方法,Lock的使用方法在下一篇博文中講述。

synchronized同步方法

synchronized是Java語言的關鍵字,當它用來修飾一個方法或者一個程式碼塊的時候,能夠保證在同一時刻最多隻有一個執行緒執行該段程式碼。在瞭解synchronized關鍵字的使用方法之前,我們先來看一個概念:互斥鎖,顧名思義:能到達到互斥訪問目的的鎖。

舉個簡單的例子:如果對臨界資源加上互斥鎖,當一個執行緒在訪問該臨界資源時,其他執行緒便只能等待。

在Java中,每一個物件都擁有一個鎖標記(monitor),也稱為監視器,多執行緒同時訪問某個物件時,執行緒只有獲取了該物件的鎖才能訪問。

在Java中,可以使用synchronized關鍵字來標記一個方法或者程式碼塊,當某個執行緒呼叫該物件的synchronized方法或者訪問synchronized程式碼塊時,這個執行緒便獲得了該物件的鎖,其他執行緒暫時無法訪問這個方法,只有等待這個方法執行完畢或者程式碼塊執行完畢,這個執行緒才會釋放該物件的鎖,其他執行緒才能執行這個方法或者程式碼塊。

synchronized的使用

  • synchronized程式碼塊,被修飾的程式碼成為同步語句塊,其作用的範圍是呼叫這個程式碼塊的物件,我們在用synchronized關鍵字的時候,能縮小程式碼段的範圍就儘量縮小,能在程式碼段上加同步就不要再整個方法上加同步。這叫減小鎖的粒度,使程式碼更大程度的併發。

  • synchronized方法,被修飾的方法成為同步方法,其作用範圍是整個方法,作用物件是呼叫這個方法的物件。

  • synchronized靜態方法,修飾一個static靜態方法,其作用範圍是整個靜態方法,作用物件是這個類的所有物件。

  • synchronized類,其作用範圍是Synchronized後面括號括起來的部分synchronized(className.class),作用的物件是這個類的所有物件。

  • synchronized(),()中是鎖住的物件, synchronized(this)鎖住的只是物件本身,同一個類的不同物件呼叫的synchronized方法並不會被鎖住,而synchronized(className.class)實現了全域性鎖的功能,所有這個類的物件呼叫這個方法都受到鎖的影響,此外()中還可以新增一個具體的物件,實現給具體物件加鎖。

    synchronized (object) {
    //在同步程式碼塊中對物件進行操作
    }複製程式碼

synchronized注意事項

  • 當兩個併發執行緒訪問同一個物件中的synchronized程式碼塊時,在同一時刻只能有一個執行緒得到執行,另一個執行緒受阻塞,必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。兩個執行緒間是互斥的,因為在執行synchronized程式碼塊時會鎖定當前的物件,只有執行完該程式碼塊才能釋放該物件鎖,下一個執行緒才能執行並鎖定該物件。

  • 當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該object中的非synchronized(this)同步程式碼塊。(兩個執行緒使用的是同一個物件)

  • 當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,其他執行緒對object中所有其它synchronized(this)同步程式碼塊的訪問將被阻塞(同上,兩個執行緒使用的是同一個物件)。

下面通過程式碼來實現:

1)當兩個併發執行緒訪問同一個物件object中的這個synchronized(this)同步程式碼塊時,一個時間內只能有一個執行緒得到執行。另一個執行緒必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。

package ths;

public class Thread1 implements Runnable {  
     public void run() {  
          synchronized(this) {  
               for (int i = 0; i < 5; i++) {  
                    System.out.println(Thread.currentThread().getName() + " synchronized loop " + i);  
               }  
          }  
     }  
     public static void main(String[] args) {  
          Thread1 t1 = new Thread1();  
          Thread ta = new Thread(t1, "A");  
          Thread tb = new Thread(t1, "B");  
          ta.start();  
          tb.start();  
     } 
}複製程式碼

輸出結果:

A synchronized loop 0  
A synchronized loop 1  
A synchronized loop 2  
A synchronized loop 3  
A synchronized loop 4  
B synchronized loop 0  
B synchronized loop 1  
B synchronized loop 2  
B synchronized loop 3  
B synchronized loop 4複製程式碼

2)然而,當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該object中的非synchronized(this)同步程式碼塊。

package ths;

public class Thread2 {  
     public void m4t1() {  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }  
     }  
     public void m4t2() {  
          int i = 5;  
          while( i-- > 0) {  
               System.out.println(Thread.currentThread().getName() + " : " + i);  
               try {  
                    Thread.sleep(500);  
               } catch (InterruptedException ie) {  
               }  
          }  
     }  
     public static void main(String[] args) {  
          final Thread2 myt2 = new Thread2();  
          Thread t1 = new Thread(  new Runnable() {  public void run() {  myt2.m4t1();  }  }, "t1"  );  
          Thread t2 = new Thread(  new Runnable() {  public void run() { myt2.m4t2();   }  }, "t2"  );  
          t1.start();  
          t2.start();  
     } 
}複製程式碼

輸出結果:

t1 : 4  
t2 : 4  
t1 : 3  
t2 : 3  
t1 : 2  
t2 : 2  
t1 : 1  
t2 : 1  
t1 : 0  
t2 : 0複製程式碼

3)尤其關鍵的是,當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,其他執行緒對object中所有其它synchronized(this)同步程式碼塊的訪問將被阻塞。

//修改Thread2.m4t2()方法:  
     public void m4t2() {  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }

     }複製程式碼

輸出結果:

t1 : 4  
t1 : 3  
t1 : 2  
t1 : 1  
t1 : 0  
t2 : 4  
t2 : 3  
t2 : 2  
t2 : 1  
t2 : 0複製程式碼

4)第三個例子同樣適用其它同步程式碼塊。也就是說,當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,它就獲得了這個object的物件鎖。結果,其它執行緒對該object物件所有同步程式碼部分的訪問都被暫時阻塞。

 //修改Thread2.m4t2()方法如下:

     public synchronized void m4t2() {  
          int i = 5;  
          while( i-- > 0) {  
               System.out.println(Thread.currentThread().getName() + " : " + i);  
               try {  
                    Thread.sleep(500);  
               } catch (InterruptedException ie) {  
               }  
          }  
     }複製程式碼

輸出結果:

t1 : 4  
t1 : 3  
t1 : 2  
t1 : 1  
t1 : 0  
t2 : 4  
t2 : 3  
t2 : 2  
t2 : 1  
t2 : 0複製程式碼

5)每個類也會有一個鎖,它可以用來控制對static資料成員的併發訪問。
並且如果一個執行緒執行一個物件的非static synchronized方法,另外一個執行緒需要執行這個物件所屬類的static synchronized方法,此時不會發生互斥現象,因為訪問static synchronized方法佔用的是類鎖,而訪問非static synchronized方法佔用的是物件鎖,所以不存在互斥現象。
程式碼如下:

public class Test {

    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            @Override
            public void run() {
                insertData.insert();
            }
        }.start(); 
        new Thread(){
            @Override
            public void run() {
                insertData.insert1();
            }
        }.start();
    }  
}

class InsertData { 
    public synchronized void insert(){
        System.out.println("執行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行insert完畢");
    }

    public synchronized static void insert1() {
        System.out.println("執行insert1");
        System.out.println("執行insert1完畢");
    }
}複製程式碼

輸出結果:

執行insert
執行insert1
執行insert1完畢
執行insert完畢複製程式碼

第一個執行緒裡面執行的是insert方法,不會導致第二個執行緒執行insert1方法發生阻塞現象。

面試題

當一個執行緒進入一個物件的synchronized方法A之後,其它執行緒是否可進入此物件的synchronized方法B?
答:不能。其它執行緒只能訪問該物件的非同步方法,同步方法則不能進入。因為非靜態方法上的synchronized修飾符要求執行方法時要獲得物件的鎖,如果已經進入A方法說明物件鎖已經被取走,那麼試圖進入B方法的執行緒就只能在等鎖池(注意不是等待池哦)中等待物件的鎖。

synchronized關鍵字的用法?
答:synchronized關鍵字可以將物件或者方法標記為同步,以實現對物件和方法的互斥訪問,可以用synchronized(物件) { … }定義同步程式碼塊,或者在宣告方法時將synchronized作為方法的修飾符。

簡述synchronized 和java.util.concurrent.locks.Lock的異同?
答:Lock是Java 5以後引入的新的API,和關鍵字synchronized相比主要相同點:Lock 能完成synchronized所實現的所有功能;主要不同點:Lock有比synchronized更精確的執行緒語義和更好的效能,而且不強制性的要求一定要獲得鎖。synchronized會自動釋放鎖,而Lock一定要求程式設計師手工釋放,並且最好在finally 塊中釋放(這是釋放外部資源的最好的地方)

總結

以上就是synchronized的概念和基本使用用法,下一篇博文中將介紹Lock,希望對你有所幫助。


一直覺得自己寫的不是技術,而是情懷,一篇篇文章是自己這一路走來的痕跡。靠專業技能的成功是最具可複製性的,希望我的這條路能讓你少走彎路,希望我能幫你抹去知識的蒙塵,希望我能幫你理清知識的脈絡,希望未來技術之巔上有你也有我。

訂閱博主微信公眾號:嘟爺java超神學堂(javaLearn)三大好處:

  • 獲取最新博主部落格更新資訊,首發公眾號
  • 獲取大量視訊,電子書,精品破解軟體資源
  • 可以跟博主聊天,歡迎程式媛妹妹來撩我

Java 多執行緒乾貨系列—(二)synchronized | 掘金技術徵文

掘金技術徵文第三期:聊聊你的最佳實踐

相關文章