帶你深入理解和解剖 synchronized

charlven發表於2020-04-05

1. 是什麼?

synchronized 是 JAVA 關鍵字,用在方法或程式碼塊。控制方法或程式碼同一時間只有一個執行緒執行,用來解決多執行緒同時訪問出現的併發問題。

2. 使用分類(作用域)

物件鎖(程式碼塊)

  • 表現形式1:加在方法上
private synchronized boolean sellTicketsWithSyncMethod(int count) {
    if (totalTickets - count < 0) {
        return false;
    } else {
        totalTickets = totalTickets - count;
        return true;
    }
}
複製程式碼
  • 表現形式2:加在物件上

鎖多個物件時,相互不干擾

private final Object LOCK = new Object();
private boolean sellTicketsWithSyncObject(int count) {
    synchronized (LOCK) {
        if (totalTickets - count < 0) {
            return false;
        } else {
            totalTickets = totalTickets - count;
            return true;
        }
    }
}
複製程式碼
  • 表現形式3:this當前物件上
private boolean sellTicketsWithSyncObject(int count) {
    synchronized (this) {
        if (totalTickets - count < 0) {
            return false;
        } else {
            totalTickets = totalTickets - count;
            return true;
        }
    }
}
複製程式碼

類鎖

  • 表現形式1:static
public static synchronized void staticMethod() {
   // todo
}
複製程式碼
  • 表現形式2:synchronized(xxx.class)
public Class SyncClass {
    private void method () {
        synchronized (SyncClass.class) {
            //todo
        }
    }
}
複製程式碼

物件鎖和類鎖區別?

物件鎖鎖的是一個物件。不同物件下互不干擾。而類只有一個。

3. synchronized 的特徵

  1. 它是一個可重入鎖。
  2. JAVA 語言的關鍵字,由虛擬機器位元組碼指令實現。(下面深入講解)
    1. 用 monitor 來控制進出
    2. moniterenter 進入
    3. Monitorexit 為退出
  3. 不可中斷,無法控制鎖的超時時間,可能會造成阻塞。而 ReentranLock 可以,這個後面講解。
  4. synchronized 只有非公平鎖。 公平鎖和非公平鎖的區別
  5. 粒度是物件,鎖的是物件而非變數。

3.1 可重入講解

指的是同一執行緒的外層函式獲得鎖之後,內層函式可以直接再次獲取該鎖。從而避免死鎖。

演示:

public class SynchronizedReentrantTest {
    int i = 0;
    public synchronized void method1() {
        method2();
    }
 
    private synchronized void method2() {
    }
 
    private synchronized void reentrantSelf() {
        if (i == 0) {
            i++;
            reentrantSelf();
        }
    }
 
    public static void main(String[] args) {
        SynchronizedReentrantTest instance = new SynchronizedReentrantTest();
        
        // 重入其他方法
        instance.method1();
        
        // 自己重入自己
        instance.reentrantSelf();
        
        // 當然也可以重入其他類方法,這裡不演示了。
    }
}
複製程式碼

4. 深入理解 synchronized (反編譯來看看)

如何反編譯?

  1. 編譯 javac xxxx.java
  2. 反編譯 javap -c -p -v xxxx.class

以下是擷取的部分原始碼:

public class SynchronizedMethodDif {
    //...

    // 作用於物件上
    private static final Object LOCK = new Object();
    private static boolean sellTicketsWithSyncObject(int count) {
        synchronized (LOCK) {
            if (totalTickets - count < 0) {
                return false;
            } else {
                totalTickets = totalTickets - count;
                return true;
            }
        }
    }
    
    // 作用於 static 方法上
    private static synchronized void soutStatic() {
        System.out.println(Thread.currentThread().getName() + " is sleeping ");
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " : 1");
        System.out.println(Thread.currentThread().getName() + " finish sleeping ");
    }
    
    // 作用於普通方法上
    private synchronized void soutNonStatic() {
        System.out.println(Thread.currentThread().getName() + " is sleeping ");
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " : 1");
        System.out.println(Thread.currentThread().getName() + " finish sleeping ");
    }
    
    //...
}
複製程式碼

反編譯後:

// 作用於物件上
private static boolean sellTicketsWithSyncObject(int);
descriptor: (I)Z
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
Code:
  stack=2, locals=3, args_size=1
    0: getstatic     #10                 // Field LOCK:Ljava/lang/Object;
    3: dup
    4: astore_1
    5: monitorenter
    ...
    16: monitorexit
    17: ireturn
    ...
    27: aload_1
    28: monitorexit
    29: ireturn
    ...
    31: aload_1
    32: monitorexit
    33: aload_2
    34: athrow
    ...
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/Throwable ]

...
// 作用於 static 上
private static synchronized void soutStatic();
    descriptor: ()V
    flags: (0x002a) ACC_PRIVATE, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    ...
    frame_type = 4 /* same */

...
// 作用於 普通方法 上
private synchronized void soutNonStatic();
    descriptor: ()V
    flags: (0x0022) ACC_PRIVATE, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    ...
    frame_type = 4 /* same */

複製程式碼

4.1 原理分析

-> 作用於 程式碼塊(物件) 上:

通過 monitor 來控制,使用monitorenter 和 monitorexit 指令控制執行緒進出。

monitor 講解:

是一個監視器,執行是依賴於物件的鎖計數

  1. monitorenter:每個物件都是一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
    1. 如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者
    2. 如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1
    3. 如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權
  2. monitorexit:執行monitorexit的執行緒必須是objectref所對應的monitor的所有者。指令執行時,monitor的進入數減1,如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權。

-> 作用於 方法

  • 方法使用 ACC_SYNCHRONIZED 標識
  • static 方法,則鎖是作用在類上
  • 非 static 方法,則鎖作用在類物件上

相關文章