1. 程式人生 > >Java同步關鍵字synchronized詳解

Java同步關鍵字synchronized詳解

前言

多執行緒程式設計可以極大地提高了效率,但也會帶來執行緒安全問題。比如說多個執行緒向資料庫插入資料,就可能會導致資料庫中資料重複。

什麼時候會引發執行緒安全問題

首先我需要了解什麼是臨界資源?有這樣一種資源,在某一時刻只能被一個執行緒所使用,這種資源可以是各種型別的的資源:一個變數、一個物件、一個檔案、一個數據庫表等。

舉個簡單的例子:

現在有兩個執行緒分別從網路上讀取資料,然後插入一張資料庫表中,要求不能插入重複的資料。那麼必然在插入資料的過程中存在兩個操作:
1)檢查資料庫中是否存在該條資料;
2)如果存在,則不插入;如果不存在,則插入到資料庫中。假如兩個執行緒分別用thread-a和thread-b表示,某一時刻,thread-a和thread-b都讀取到了資料X,那麼可能會發生這種情況:thread-a去檢查資料庫中是否存在資料X,然後thread-b也接著去檢查資料庫中是否存在資料X,如果資料x不存在,就會導致重複插入。

如何解決執行緒安全問題

基本上所有的併發模式在解決執行緒安全問題時,都採用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個執行緒訪問臨界資源,也稱作同步互斥訪問。通常來說,是在訪問臨界資源的程式碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他執行緒繼續訪問,即通過加鎖實現

synchronized同步方法或者同步塊

在瞭解synchronized關鍵字的使用方法之前,我們先來看一個概念:互斥鎖,顧名思義:能到達到互斥訪問目的的鎖。舉個簡單的例子:如果對臨界資源加上互斥鎖,當一個執行緒在訪問該臨界資源時,其他執行緒便只能等待。 在Java中,每一個物件都擁有一個鎖標記(monitor),也稱為監視器,多執行緒同時訪問某個物件時,執行緒只有獲取了該物件的鎖才能訪問。

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

下面通過幾個簡單的例子來說明synchronized關鍵字的使用:

synchronized方法

下面這段程式碼中兩個執行緒分別呼叫insertData物件插入資料:

public class Test {
    public
static void main(String[] args) { final InsertData insertData = new InsertData(); new Thread(() -> insertData.insert(Thread.currentThread())).start(); new Thread(() -> insertData.insert(Thread.currentThread())).start(); } } class InsertData { private List<Integer> arrayList = new ArrayList<Integer>(); public void insert(Thread thread) { for (int i = 0; i < 5; i++) { System.out.println(thread.getName() + "在插入資料" + i); arrayList.add(i); } } }

輸出結果
在這裡插入圖片描述

說明兩個執行緒在同時執行insert方法。而如果在insert方法前面加上關鍵字synchronized的話,執行結果為:

class InsertData {
    private List<Integer> arrayList = new ArrayList<Integer>();

    public synchronized void insert(Thread thread) {
        for (int i = 0; i < 5; i++) {
            System.out.println(thread.getName() + "在插入資料" + i);
            arrayList.add(i);
        }
    }
}

在這裡插入圖片描述

從上輸出結果說明,Thread-0插入資料是等Thread-1插入完資料之後才進行的。說明Thread-0和Thread-1是順序執行insert方法的。
這就是synchronized方法。
不過有幾點需要注意:

  1. 當一個執行緒正在訪問一個物件的synchronized方法,那麼其他執行緒不能訪問該物件的其他synchronized方法。這個原因很簡單,因為一個物件只有一把鎖,當一個執行緒獲取了該物件的鎖之後,其他執行緒無法獲取該物件的鎖,所以無法訪問該物件的其他synchronized方法。
  2. 當一個執行緒正在訪問一個物件的synchronized方法,那麼其他執行緒能訪問該物件的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該物件的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那麼其他執行緒是可以訪問這個方法的,
  3. 如果一個執行緒A需要訪問物件object1的synchronized方法fun1,另外一個執行緒B需要訪問物件object2的synchronized方法fun1,即使object1和object2是同一型別),也不會產生執行緒安全問題,因為他們訪問的是不同的物件,所以不存在互斥問題。

synchronized程式碼塊

synchronized程式碼塊類似於以下這種形式:

synchronized(synObject) {
 // 程式碼邏輯
}

當在某個執行緒中執行這段程式碼塊,該執行緒會獲取物件synObject的鎖,從而使得其他執行緒無法同時訪問該程式碼塊。synObject可以是this,代表獲取當前物件的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。

比如上面的insert方法可以改成以下兩種形式:

class InsertData {
    private List<Integer> arrayList = new ArrayList<Integer>();
    public void insert(Thread thread) {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(thread.getName() + "在插入資料" + i);
                arrayList.add(i);
            }
        }
    }
}
class InsertData {
    private List<Integer> arrayList = new ArrayList<Integer>();
    private Object object = new Object();
    public void insert(Thread thread) {
        synchronized (object) {
            for (int i = 0; i < 5; i++) {
                System.out.println(thread.getName() + "在插入資料" + i);
                arrayList.add(i);
            }
        }
    }
}

從上面可以看出,synchronized程式碼塊使用起來比synchronized方法要靈活得多。因為也許一個方法中只有一部分程式碼只需要同步,如果此時對整個方法用synchronized進行同步,會影響程式執行效率。而使用synchronized程式碼塊就可以避免這個問題,synchronized程式碼塊可以實現只對需要同步的地方進行同步。

另外,每個類也會有一個鎖,它可以用來控制對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(() -> insertData.insert()).start();
        new Thread(() -> InsertData.insert1()).start();
    }
}

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

    public synchronized static void insert1() {
        System.out.println("執行insert1");
        System.out.println("執行insert1完畢");
    }
}

輸出結果
在這裡插入圖片描述
第一個執行緒裡面執行的是insert方法,不會導致第二個執行緒執行insert1方法發生阻塞現象。

下面我們看一下synchronized關鍵字到底做了什麼事情,我們來反編譯它的位元組碼看一下,下面這段程式碼反編譯後的位元組碼為

public class InsertDatas {
    private Object object = new Object();

    public void insert(Thread thread) {
        synchronized (object) {
        }
    }
    public synchronized void insert1(Thread thread) {
    }

    public void insert2(Thread thread) {
    }
}

在這裡插入圖片描述
從反編譯獲得的位元組碼可以看出,synchronized程式碼塊實際上多了monitorenter和monitorexit兩條指令。monitorenter指令執行時會讓物件的鎖計數加1,而monitorexit指令執行時會讓物件的鎖計數減1,其實這個與作業系統裡面的PV操作很像,作業系統裡面的PV操作就是用來控制多個執行緒對臨界資源的訪問。對於synchronized方法,執行中的執行緒識別該方法的 method_info 結構是否有 ACC_SYNCHRONIZED 標記設定,然後它自動獲取物件的鎖,呼叫方法,最後釋放鎖。如果有異常發生,執行緒自動釋放鎖。

有一點要注意:對於synchronized方法或者synchronized程式碼塊,當出現異常時,JVM會自動釋放當前執行緒佔用的鎖,因此不會由於異常導致出現死鎖現象。