Java 之 synchronized 詳解

zly394發表於2017-06-21

一、概念

synchronized 是 Java 中的關鍵字,是利用鎖的機制來實現同步的。

鎖機制有如下兩種特性:

  • 互斥性:即在同一時間只允許一個執行緒持有某個物件鎖,通過這種特性來實現多執行緒中的協調機制,這樣在同一時間只有一個執行緒對需同步的程式碼塊(複合操作)進行訪問。互斥性我們也往往稱為操作的原子性。

  • 可見性:必須確保在鎖被釋放之前,對共享變數所做的修改,對於隨後獲得該鎖的另一個執行緒是可見的(即在獲得鎖時應獲得最新共享變數的值),否則另一個執行緒可能是在本地快取的某個副本上繼續操作從而引起不一致。

二、物件鎖和類鎖

1. 物件鎖

在 Java 中,每個物件都會有一個 monitor 物件,這個物件其實就是 Java 物件的鎖,通常會被稱為“內建鎖”或“物件鎖”。類的物件可以有多個,所以每個物件有其獨立的物件鎖,互不干擾。

2. 類鎖

在 Java 中,針對每個類也有一個鎖,可以稱為“類鎖”,類鎖實際上是通過物件鎖實現的,即類的 Class 物件鎖。每個類只有一個 Class 物件,所以每個類只有一個類鎖。

三、synchronized 的用法分類

synchronized 的用法可以從兩個維度上面分類:

1. 根據修飾物件分類

synchronized 可以修飾方法和程式碼塊

  • 修飾程式碼塊

    • synchronized(this|object) {}

    • synchronized(類.class) {}

  • 修飾方法

    • 修飾非靜態方法

    • 修飾靜態方法

2. 根據獲取的鎖分類

  • 獲取物件鎖

    • synchronized(this|object) {}

    • 修飾非靜態方法

  • 獲取類鎖

    • synchronized(類.class) {}

    • 修飾靜態方法

四、synchronized 的用法詳解

這裡根據獲取的鎖分類來分析 synchronized 的用法

1. 獲取物件鎖

1.1 對於同一物件

  • 同步執行緒類:
class SyncThread implements Runnable {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.startsWith("A")) {
            async();
        } else if (threadName.startsWith("B")) {
            sync1();
        } else if (threadName.startsWith("C")) {
            sync2();
        }

    }

    /**
     * 非同步方法
     */
    private void async() {
        try {
            System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 方法中有 synchronized(this|object) {} 同步程式碼塊
     */
    private void sync1() {
        System.out.println(Thread.currentThread().getName() + "_Sync1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (this) {
            try {
                System.out.println(Thread.currentThread().getName() + "_Sync1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "_Sync1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * synchronized 修飾非靜態方法
     */
    private synchronized void sync2() {
        System.out.println(Thread.currentThread().getName() + "_Sync2: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        try {
            System.out.println(Thread.currentThread().getName() + "_Sync2_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "_Sync2_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}複製程式碼
  • 測試程式碼:
public class SyncTest {

    public static void main(String... args) {
        SyncThread syncThread = new SyncThread();
        Thread A_thread1 = new Thread(syncThread, "A_thread1");
        Thread A_thread2 = new Thread(syncThread, "A_thread2");
        Thread B_thread1 = new Thread(syncThread, "B_thread1");
        Thread B_thread2 = new Thread(syncThread, "B_thread2");
        Thread C_thread1 = new Thread(syncThread, "C_thread1");
        Thread C_thread2 = new Thread(syncThread, "C_thread2");
        A_thread1.start();
        A_thread2.start();
        B_thread1.start();
        B_thread2.start();
        C_thread1.start();
        C_thread2.start();
    }
}複製程式碼
  • 執行結果:
B_thread2_Sync1: 14:44:20
A_thread1_Async_Start: 14:44:20
B_thread1_Sync1: 14:44:20
C_thread1_Sync2: 14:44:20
A_thread2_Async_Start: 14:44:20
C_thread1_Sync2_Start: 14:44:20
A_thread1_Async_End: 14:44:22
A_thread2_Async_End: 14:44:22
C_thread1_Sync2_End: 14:44:22
B_thread1_Sync1_Start: 14:44:22
B_thread1_Sync1_End: 14:44:24
B_thread2_Sync1_Start: 14:44:24
B_thread2_Sync1_End: 14:44:26
C_thread2_Sync2: 14:44:26
C_thread2_Sync2_Start: 14:44:26
C_thread2_Sync2_End: 14:44:28複製程式碼
  • 結果分析:

    • A 類執行緒訪問方法中沒有同步程式碼塊,A 類執行緒是非同步的,所以有執行緒訪問物件的同步程式碼塊時,另外的執行緒可以訪問該物件的非同步程式碼塊:

      A_thread1_Async_Start: 14:44:20
      A_thread2_Async_Start: 14:44:20
      A_thread1_Async_End: 14:44:22
      A_thread2_Async_End: 14:44:22複製程式碼
    • B 類執行緒訪問的方法中有同步程式碼塊,B 類執行緒是同步的,一個執行緒在訪問物件的同步程式碼塊,另一個訪問物件的同步程式碼塊的執行緒會被阻塞:

      B_thread1_Sync1_Start: 14:44:22
      B_thread1_Sync1_End: 14:44:24
      B_thread2_Sync1_Start: 14:44:24
      B_thread2_Sync1_End: 14:44:26複製程式碼
    • synchronized(this|object) {} 程式碼塊 {} 之外的程式碼依然是非同步的:

      B_thread2_Sync1: 14:44:20
      B_thread1_Sync1: 14:44:20複製程式碼
    • C 類執行緒訪問的是 synchronized 修飾非靜態方法,C 類執行緒是同步的,一個執行緒在訪問物件的同步代方法,另一個訪問物件同步方法的執行緒會被阻塞:

      C_thread1_Sync2_Start: 14:44:20
      C_thread1_Sync2_End: 14:44:22
      C_thread2_Sync2_Start: 14:44:26
      C_thread2_Sync2_End: 14:44:28複製程式碼
    • synchronized 修飾非靜態方法,作用範圍是整個方法,所以方法中所有的程式碼都是同步的:

      C_thread1_Sync2: 14:44:20
      C_thread2_Sync2: 14:44:26複製程式碼
    • 由結果可知 B 類和 C 類執行緒順序執行,類中 synchronized(this|object) {} 程式碼塊和 synchronized 修飾非靜態方法獲取的鎖是同一個鎖,即該類的物件的物件鎖。所以 B 類執行緒和 C 類執行緒也是同步的:

      B_thread1_Sync1_Start: 14:44:22
      B_thread1_Sync1_End: 14:44:24
      C_thread1_Sync2_Start: 14:44:20
      C_thread1_Sync2_End: 14:44:22
      B_thread2_Sync1_Start: 14:44:24
      B_thread2_Sync1_End: 14:44:26
      C_thread2_Sync2_Start: 14:44:26
      C_thread2_Sync2_End: 14:44:28複製程式碼

1.2 對於不同物件

  • 修改測試程式碼為:
public class SyncTest {

    public static void main(String... args) {
        Thread A_thread1 = new Thread(new SyncThread(), "A_thread1");
        Thread A_thread2 = new Thread(new SyncThread(), "A_thread2");
        Thread B_thread1 = new Thread(new SyncThread(), "B_thread1");
        Thread B_thread2 = new Thread(new SyncThread(), "B_thread2");
        Thread C_thread1 = new Thread(new SyncThread(), "C_thread1");
        Thread C_thread2 = new Thread(new SyncThread(), "C_thread2");
        A_thread1.start();
        A_thread2.start();
        B_thread1.start();
        B_thread2.start();
        C_thread1.start();
        C_thread2.start();
    }
}複製程式碼
  • 執行結果:
A_thread2_Async_Start: 15:01:34
C_thread2_Sync2: 15:01:34
B_thread2_Sync1: 15:01:34
C_thread1_Sync2: 15:01:34
B_thread2_Sync1_Start: 15:01:34
B_thread1_Sync1: 15:01:34
C_thread1_Sync2_Start: 15:01:34
A_thread1_Async_Start: 15:01:34
C_thread2_Sync2_Start: 15:01:34
B_thread1_Sync1_Start: 15:01:34
C_thread1_Sync2_End: 15:01:36
A_thread1_Async_End: 15:01:36
C_thread2_Sync2_End: 15:01:36
B_thread2_Sync1_End: 15:01:36
B_thread1_Sync1_End: 15:01:36
A_thread2_Async_End: 15:01:36複製程式碼
  • 結果分析:

    • 兩個執行緒訪問不同物件的 synchronized(this|object) {} 程式碼塊和 synchronized 修飾非靜態方法是非同步的,同一個類的不同物件的物件鎖互不干擾。

2 獲取類鎖

2.1 對於同一物件

  • 同步執行緒類:
class SyncThread implements Runnable {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.startsWith("A")) {
            async();
        } else if (threadName.startsWith("B")) {
            sync1();
        } else if (threadName.startsWith("C")) {
            sync2();
        }

    }

    /**
     * 非同步方法
     */
    private void async() {
        try {
            System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 方法中有 synchronized(類.class) {} 同步程式碼塊
     */
    private void sync1() {
        System.out.println(Thread.currentThread().getName() + "_Sync1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (SyncThread.class) {
            try {
                System.out.println(Thread.currentThread().getName() + "_Sync1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + "_Sync1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * synchronized 修飾靜態方法
     */
    private synchronized static void sync2() {
        System.out.println(Thread.currentThread().getName() + "_Sync2: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        try {
            System.out.println(Thread.currentThread().getName() + "_Sync2_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "_Sync2_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}複製程式碼
  • 測試程式碼:
public class SyncTest {

    public static void main(String... args) {
        SyncThread syncThread = new SyncThread();
        Thread A_thread1 = new Thread(syncThread, "A_thread1");
        Thread A_thread2 = new Thread(syncThread, "A_thread2");
        Thread B_thread1 = new Thread(syncThread, "B_thread1");
        Thread B_thread2 = new Thread(syncThread, "B_thread2");
        Thread C_thread1 = new Thread(syncThread, "C_thread1");
        Thread C_thread2 = new Thread(syncThread, "C_thread2");
        A_thread1.start();
        A_thread2.start();
        B_thread1.start();
        B_thread2.start();
        C_thread1.start();
        C_thread2.start();
    }
}複製程式碼
  • 執行結果:
B_thread1_Sync1: 15:08:13
C_thread1_Sync2: 15:08:13
B_thread2_Sync1: 15:08:13
A_thread1_Async_Start: 15:08:13
C_thread1_Sync2_Start: 15:08:13
A_thread2_Async_Start: 15:08:13
C_thread1_Sync2_End: 15:08:15
A_thread2_Async_End: 15:08:15
A_thread1_Async_End: 15:08:15
B_thread2_Sync1_Start: 15:08:15
B_thread2_Sync1_End: 15:08:17
B_thread1_Sync1_Start: 15:08:17
B_thread1_Sync1_End: 15:08:19
C_thread2_Sync2: 15:08:19
C_thread2_Sync2_Start: 15:08:19
C_thread2_Sync2_End: 15:08:21複製程式碼
  • 結果分析:

    • 由結果可以看出,在同一物件的情況下,synchronized(類.class) {} 程式碼塊或 synchronized 修飾靜態方法和 synchronized(this|object) {} 程式碼塊和 synchronized 修飾非靜態方法的行為一致。

2.2 對於不同物件

  • 修改測試程式碼為:
public class SyncTest {

    public static void main(String... args) {
        Thread A_thread1 = new Thread(new SyncThread(), "A_thread1");
        Thread A_thread2 = new Thread(new SyncThread(), "A_thread2");
        Thread B_thread1 = new Thread(new SyncThread(), "B_thread1");
        Thread B_thread2 = new Thread(new SyncThread(), "B_thread2");
        Thread C_thread1 = new Thread(new SyncThread(), "C_thread1");
        Thread C_thread2 = new Thread(new SyncThread(), "C_thread2");
        A_thread1.start();
        A_thread2.start();
        B_thread1.start();
        B_thread2.start();
        C_thread1.start();
        C_thread2.start();
    }
}複製程式碼
  • 執行結果:
A_thread2_Async_Start: 15:17:28
B_thread2_Sync1: 15:17:28
A_thread1_Async_Start: 15:17:28
B_thread1_Sync1: 15:17:28
C_thread1_Sync2: 15:17:28
C_thread1_Sync2_Start: 15:17:28
C_thread1_Sync2_End: 15:17:30
A_thread2_Async_End: 15:17:30
B_thread1_Sync1_Start: 15:17:30
A_thread1_Async_End: 15:17:30
B_thread1_Sync1_End: 15:17:32
B_thread2_Sync1_Start: 15:17:32
B_thread2_Sync1_End: 15:17:34
C_thread2_Sync2: 15:17:34
C_thread2_Sync2_Start: 15:17:34
C_thread2_Sync2_End: 15:17:36複製程式碼
  • 結果分析:

    • 兩個執行緒訪問不同物件的 synchronized(類.class) {} 程式碼塊或 synchronized 修飾靜態方法還是同步的,類中 synchronized(類.class) {} 程式碼塊和 synchronized 修飾靜態方法獲取的鎖是類鎖。對於同一個類的不同物件的類鎖是同一個。

3 類中同時有 synchronized(類.class) {} 程式碼塊或 synchronized 修飾靜態方法和 synchronized(this|object) {} 程式碼塊和 synchronized 修飾非靜態方法時會怎樣?

  • 修改同步執行緒類:
class SyncThread implements Runnable {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.startsWith("A")) {
            async();
        } else if (threadName.startsWith("B")) {
            sync1();
        } else if (threadName.startsWith("C")) {
            sync2();
        }

    }

    /**
     * 非同步方法
     */
    private void async() {
        try {
            System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * synchronized 修飾非靜態方法
     */
    private synchronized void sync1() {
        try {
            System.out.println(Thread.currentThread().getName() + "_Sync1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "_Sync1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * synchronized 修飾靜態方法
     */
    private synchronized static void sync2() {
        try {
            System.out.println(Thread.currentThread().getName() + "_Sync2_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "_Sync2_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}複製程式碼
  • 修改測試程式碼:
public class SyncTest {

    public static void main(String... args) {
        Thread B_thread1 = new Thread(syncThread, "B_thread1");
        Thread C_thread1 = new Thread(syncThread, "C_thread1");
        B_thread1.start();
        C_thread1.start();
    }
}複製程式碼
  • 執行結果:
B_thread1_Sync1_Start: 15:35:21
C_thread1_Sync2_Start: 15:35:21
B_thread1_Sync1_End: 15:35:23
C_thread1_Sync2_End: 15:35:23複製程式碼
  • 執行結果分析:

    • 由結果可以看到 B 類執行緒和 C 類執行緒是非同步的,即 synchronized 修飾靜態方法和 synchronized 修飾非靜態方法是非同步的,對於 synchronized(類.class) {} 程式碼塊和 synchronized(this|object) {} 程式碼塊也是一樣的。所以物件鎖和類鎖是獨立的,互不干擾。

4 補充

  1. synchronized關鍵字不能繼承。

    對於父類中的 synchronized 修飾方法,子類在覆蓋該方法時,預設情況下不是同步的,必須顯示的使用 synchronized 關鍵字修飾才行。

  2. 在定義介面方法時不能使用synchronized關鍵字。

  3. 構造方法不能使用synchronized關鍵字,但可以使用synchronized程式碼塊來進行同步。

相關文章