深入學習Synchronized使用與原理

2020-08-09 12:16:15

前言

Synchronized在多執行緒中使用得比較多的,這兩天看了下慕課網相關課程,在此總結下其使用和原理

作用:

Synchronized據有可重入,不可中斷性,能夠保證在同一時刻最多只有一個執行緒執行該段程式碼,以達到保證併發安全的效果,內部是通過monitor來加鎖和解鎖的。

地位:

1.Synchronized是java的關鍵字,被java語言原生支援。
2.是最基本的互斥同步手段。
3.是併發程式設計中的元老級角色,是併發程式設計的必學內容。

不使用併發的後果

兩個執行緒同時a++,如果不加鎖,最後結果會比預計的少

public class DisappearRequest {

    int i = 0;

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    };

    @Test
    public void test01() throws InterruptedException {

        Thread thread = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();

        System.out.println(i);

    }
}

執行上面程式碼,期望結果是200000,但是每次執行結果都是小於這個數的。

原因:
i++不是一個原子操作,實際上包含三個動作
1.讀取i;
2.將i加1;
3.將i的值寫入到記憶體中。

在多執行緒環境下這樣是不安全的。在解決問題之前,還是先來學習下Synchronized的用法。

Synchronized的兩個用法

物件鎖

包括方法鎖(預設鎖物件爲this當前範例物件)和同步程式碼塊鎖(自己指定的鎖物件)

類鎖

指synchronized修飾靜態的方法或指定鎖物件爲class物件

一、物件鎖

1、程式碼塊形式

public Object lock = new Object();

//程式碼塊鎖
public Runnable runnable = new Runnable() {
    @Override
    public void run() {
        synchronized (lock) { //this或者自建物件
            System.out.println("我是物件鎖的程式碼塊形式,我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":執行結束");
        }
    }
};

@Test
public void test01() {

    Thread t1 = new Thread(runnable);
    Thread t2 = new Thread(runnable);
    t1.start();
    t2.start();
    while (t1.isAlive() || t2.isAlive()) {

    }
    System.out.println("finished");
}

物件鎖只對同一個物件的方法起到作用,如果是不同對象同一個方法,鎖會失效

2、方法鎖形式

//方法鎖
public Runnable runnable2 = new Runnable() {
    @Override
    public synchronized void run() {
        System.out.println("我是物件鎖的方法修飾符形式,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }
};
@Test
public void test01() {

    Thread t1 = new Thread(runnable2);
    Thread t2 = new Thread(runnable2);
    t1.start();
    t2.start();
    while (t1.isAlive() || t2.isAlive()) {

    }
    System.out.println("finished");
}

方法鎖形式,鎖物件預設爲this

二、類鎖

概念:java類可能有很多個物件,但只有一個class物件
本質:所以所謂的類鎖,不過是Class物件而已。

1、靜態方法鎖

class RunnableTest implements Runnable{

    @Override
    public void run() {
        method();
    }

    public static synchronized void method(){
        System.out.println("我是物件鎖的靜態方法修飾符形式,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

}

@Test
public void test01() {

    Thread t1 = new Thread(new RunnableTest());
    Thread t2 = new Thread(new RunnableTest());
    t1.start();
    t2.start();
    while (t1.isAlive() || t2.isAlive()) {

    }
    System.out.println("finished");
}

如果不加static,RunnableTest裏面的method方法可以同時執行

2、class物件程式碼塊

//class物件鎖
public Runnable runnable5 = new Runnable() {
    @Override
    public void run() {
        synchronized (SynchronizedTest.class){
            System.out.println("我是物件鎖的class物件修飾符形式,我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":執行結束");
        }
    }
};
@Test
public void test01() {

    Thread t1 = new Thread(runnable5);
    Thread t2 = new Thread(runnable5);
    t1.start();
    t2.start();
    while (t1.isAlive() || t2.isAlive()) {

    }
    System.out.println("finished");
}

物件鎖和類鎖學完了,前面的例子就可以很容易的解決了,這裏就不多說了。

多執行緒存取同步方法的7種情況

一、兩個執行緒同時存取一個物件的同步方法

前面的物件鎖已經學習過

二、兩個執行緒存取的是兩個物件的同步方法

前面的類鎖的靜態方法同步鎖已經學過,如果不加靜態的,那麼兩個執行緒執行毫無影響,不受幹擾

三、兩個執行緒存取的是synchronized的靜態方法

加了靜態方法就變成類鎖,會一個一個執行

四、同時存取同步方法與非同步方法

public class SynchronizedYesAndNoTest {

    private Runnable runnable = new Runnable(){
        @Override
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method();
            }else {
                method2();
            }
        }
    };

    public synchronized void method(){
        System.out.println("我是加鎖的方法,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

    public void method2(){
        System.out.println("我是沒加鎖的方法,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

    @Test
    public void test01() {

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

}

非同步方法不受影響

五、存取同一個物件的不同的普通同步方法

public class SynchronizedDifferentMethod {

    private Runnable runnable = new Runnable(){
        @Override
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method();
            }else {
                method2();
            }
        }
    };

    public synchronized void method(){
        System.out.println("我是加鎖的方法method,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

    public synchronized void method2(){
        System.out.println("我是加鎖的方法method2,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

    @Test
    public void test01() {

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

}

一個一個執行

六、同時存取靜態synchronized和非靜態synchronized方法

public class SynchronizedStaticAndNormal {

    private Runnable runnable = new Runnable(){
        @Override
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method();
            }else {
                method2();
            }
        }
    };

    public static synchronized void method(){
        System.out.println("我是靜態加鎖的方法method,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

    public synchronized void method2(){
        System.out.println("我是非靜態加鎖的方法method2,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

    @Test
    public void test01() {

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

}

不受影響,同時進行

七、方法拋出異常後,會釋放鎖

public class SynchronizedException {

    private Runnable runnable = new Runnable(){
        @Override
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method();
            }else {
                method2();
            }
        }
    };

    public synchronized void method(){
        System.out.println("我是方法method,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        throw new RuntimeException();

//        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

    public synchronized void method2(){
        System.out.println("我是方法method2,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":執行結束");
    }

    @Test
    public void test01() {

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

拋出異常後,jvm會自動釋放鎖

八、七種情況總結

1.一把鎖只能同時被一個執行緒獲取,沒有拿到鎖的執行緒必須等待(對應第1,5種情況)
2.每個範例都對應有自己的一把鎖,不同範例之前互不影響;例外:鎖物件是*.class以及synchronized修飾的是static方法的時候,所有物件共用同一把類鎖(對應第2,3,4,6種情況)
3.無論是正常執行完畢或者拋出異常,都會釋放鎖。

Synchronized的性質

一、可重入性

定義:指的是同一個執行緒的外層函數獲得鎖之後,內層函數可以直接再次獲取到鎖。(Synchronized,Lock)

好處:避免死鎖、提升封裝性

粒度:執行緒而非呼叫(用3種情況來說明和pthread的區別)

1、情況1:證明同一個方法是可重入的

public class SynchronizedRecursion {

    private int a = 0;

    private synchronized void method(){
        System.out.println("這是method1,a = " + a);
        if(a==0){
            a ++;
            method();
        }
    }

    @Test
    public void test01(){
        method();
    }

}

遞回呼叫自己可以成功

2、情況2:證明可重入不要求是同一個方法

public class SynchronizedOtherMethod {

    private synchronized void method(){
        System.out.println("這是method1");
        method2();
    }

    private synchronized void method2(){
        System.out.println("這是method2");
    }

    @Test
    public void test01(){
        method();
    }
}

3、情況3:證明可重入不要是同一個類種的

public class SynchronizedSuperClass {

    @Test
    public void test01(){
        TestClass testClass = new TestClass();
        testClass.method();
    }

}

class SuperClass{

    public synchronized void method(){
        System.out.println("我是父類別的方法");
    }

}

class TestClass extends SuperClass{

    @Override
    public synchronized void method() {
        super.method();
        System.out.println("我是子類的方法");
    }
}

二、不可中斷性

一旦這個鎖已經被別人獲取到了,如果我還想獲得,我只能選擇等待或者阻塞,直到別的執行緒釋放這個鎖。如果別人永遠不釋放鎖,那麼我就只能永遠等待下去。

相比之下,Lock可以擁有中斷的能力,第一點,如果我覺得我等的時間太長,有權中斷現在已經獲取到鎖的執行緒的執行;第二點,如果我等待的時間太長了不想再等了,也可以退出。

原理

一、加鎖和釋放鎖的原理

獲取和釋放的時機:內建鎖,每個java物件可以用做實現同步的鎖,這個鎖就是內建鎖,或者稱做監視鎖。

表達成lock的形式

public class SynchronizedToLock {

    Lock lock = new ReentrantLock();

    public synchronized void method1(){
        System.out.println("我是Synchronized形式的鎖");
    }

    public void method2(){
        lock.lock();

        try{
            System.out.println("我是lock形式的鎖");
        }finally {
            lock.unlock();
        }
    }

    @Test
    public void test01(){
        method1();
        method2();
    }

}

深入JVM看位元組碼
javac 編譯成class檔案
javap [-verbose] 反編譯

//java檔案
public class Decompilation {

    private Object object = new Object();

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

}

//反編譯之後的檔案
  Last modified 2020-8-9; size 503 bytes
  MD5 checksum 1832d1176898be312930160e30a29bf2
  Compiled from "Decompilation.java"
public class com.example.threadlocaldemo.Decompilation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#20         // java/lang/Object."<init>":()V
   #2 = Class              #21            // java/lang/Object
   #3 = Fieldref           #4.#22         // com/example/threadlocaldemo/Decompilation.object:Ljava/lang/Object;
   #4 = Class              #23            // com/example/threadlocaldemo/Decompilation
   #5 = Utf8               object
   #6 = Utf8               Ljava/lang/Object;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               insert
  #12 = Utf8               (Ljava/lang/Thread;)V
  #13 = Utf8               StackMapTable
  #14 = Class              #23            // com/example/threadlocaldemo/Decompilation
  #15 = Class              #24            // java/lang/Thread
  #16 = Class              #21            // java/lang/Object
  #17 = Class              #25            // java/lang/Throwable
  #18 = Utf8               SourceFile
  #19 = Utf8               Decompilation.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Utf8               java/lang/Object
  #22 = NameAndType        #5:#6          // object:Ljava/lang/Object;
  #23 = Utf8               com/example/threadlocaldemo/Decompilation
  #24 = Utf8               java/lang/Thread
  #25 = Utf8               java/lang/Throwable
{
  public com.example.threadlocaldemo.Decompilation();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putfield      #3                  // Field object:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 3: 0
        line 5: 4

  public void insert(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: getfield      #3                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter  //monitorenter和monitorexit兩個指令實現加鎖和釋放鎖
         7: aload_2
         8: monitorexit
         9: goto          17
        12: astore_3
        13: aload_2
        14: monitorexit
        15: aload_3
        16: athrow
        17: return
      Exception table:
         from    to  target type
             7     9    12   any
            12    15    12   any
      LineNumberTable:
        line 8: 0
        line 10: 7
        line 11: 17
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = [ class com/example/threadlocaldemo/Decompilation, class java/lang/Thread, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

monitorenter和monitorexit兩個指令實現加鎖和釋放鎖

二、可重入原理:加鎖次數計數器

1.jvm負責跟蹤物件被加鎖的次數
2.執行緒第一次給物件加鎖的時候,計數變爲1。每當這個相同的執行緒在此物件上再次獲得鎖時,計數會遞增(可重入)。
3.每當任務離開時,計數遞減,當計數爲0的時候,鎖會被完全釋放。

三、儲存可見性的原理:記憶體模型

在这里插入图片描述
在这里插入图片描述
Synchonized物件釋放鎖前,任何物件的修改都會被寫入到主記憶體,保證每次執行都是可靠的,保證可見性

缺陷

效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的執行緒
不夠靈活(讀寫鎖更靈活):加鎖和釋放的時機單一,每個鎖僅有單一的條件(某個物件),可能是不夠的
無法知道是否成功獲取到鎖

常見問題

1.使用注意點:鎖物件不能爲空(鎖資訊是儲存在物件頭中,鎖爲空,那麼鎖資訊就沒有了)、作用域不宜過大、避免死鎖
2.如何選擇Lock和Synchronized關鍵字

如果可以兩個都不要用,而是選擇java.util.courrent包中的類,優先Synchronized,避免出錯的發生

3.多執行緒存取同步方法的各種情況

思考

1.多個執行緒等待同一個

參考

Java高併發之魂:synchronized深度解析