1. 程式人生 > >Java多執行緒 - 執行緒同步 synchronized

Java多執行緒 - 執行緒同步 synchronized

synchronized是Java同步的基礎,只有徹底瞭解synchronized的物件鎖、可重入鎖...特性,將來在使用高階的ReentrantLock、ReentrantReadWriteLock才能得心應手

本文將透過大量的具體例項來了解synchronized的特性

  • 具體例項

    • 假設現在有一個MyObject物件myObject,他有methodA()和methodB()方法,創造兩個執行緒Thread1 和 Thread2 ,使他們都去執行這個myObject物件的methodA方法,程式碼如下

      class MyObject {
          public void methodA() {
              System.out.println("begin method A, 誰執行我: " + Thread.currentThread().getName());
              System.out.println("end");
          }
      ​
          public void methodB() {
              System.out.println("begin method B, 誰執行我: " + Thread.currentThread().getName());
              System.out.println("end");
          }
      }
      ​
      class Thread1 extends Thread {
          private MyObject object;
      ​
          Thread1(MyObject object) {
              this.object = object;
          }
      ​
          @Override
          public void run() {
              object.methodA(); // thread 1 執行 myObject 物件的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          private MyObject object;
      ​
          Thread2(MyObject object) {
              this.object = object;
          }
      ​
          @Override
          public void run() {
              object.methodA();  // thread 2 也執行 myObject 物件的 methodA() 方法
          }
      }
      ​
      public class Main {
          public static void main(String[] args) {
              MyObject object = new MyObject();
              Thread1 th1 = new Thread1(object);
              th1.setName("thread 1");
              Thread2 th2 = new Thread2(object);
              th2.setName("thread 2");
              th1.start();
              th2.start();
          }
      }
    • 因為methodA()方法上沒有加上synchronized關鍵字進行執行緒同步,所以thread1和thread2會爭搶著執行methodA(),所以結果如下

      begin methodA, 誰執行我: thread 1
      begin methodA, 誰執行我: thread 2
      end
      end
    • 這時如果在methodA上加上synchronized關鍵字,thread1和thread2會一個一個排隊執行methodA,結果如下

      class MyObject {
          synchronized public void methodA() { ... }
          public void methodB() { ... }
      }
      begin method A, 誰執行我: thread 1
      end
      begin method A, 誰執行我: thread 2
      end
    • 這時如果再新增一個物件MyObject物件myObject2,讓Thread2改為執行myObject2物件的mehodA()方法,則結果如下

      • 會使得結果又變為不同步的原因是因為synchronized其實是一個物件鎖,而不是程式碼鎖

      • 意思是指假設當Thread1開始執行myObject的methodA()時,實際上是鎖住了myObject這個物件的methodA()方法,而不是鎖住了class MyObject們的methodA()方法

      • 所以在jvm中,若是建立了多個例項物件,則會產生多把鎖,每個物件自己一把鎖,物件間的鎖彼此不干擾

      • 而在這個例子中,thread1取得的是myObject的物件鎖,而thread2取得的是myObject2的物件鎖,因為他們不是使用同一把鎖,所以才會使得執行緒不同步

      class MyObject {
          synchronized public void methodA() { ... }
          public void methodB() { ... }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 執行的是 myObject 物件的methodA()方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodA(); //thread 2 執行的是 myObject2 物件的methodA()方法
          }
      }
      begin method A, 誰執行我: thread 1
      begin method A, 誰執行我: thread 2
      end
      end
    • 如果將myObject2刪了,改回Thread1和Thread2都使用myObject這個物件,並且改成Thread1執行methodA(),Thread2執行methodB(),注意methodA有synchronized,但methodB沒有,其結果如下

      • 會使得執行緒又不同步的原因是因為methodB是一個非同步的方法,即是methodB沒有加上synchronized關鍵字

      • 因此雖然thread1已經先搶到synchonized鎖了,但是因為methodB不是一個執行緒同步的方法,因此他根本不care什麼物件鎖,直接就能夠被呼叫了

      class MyObject {
          synchronized public void methodA() { ... }
          public void methodB() { ... }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 執行myObject物件的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 執行myObject物件的 methodB() 方法
          }
      }
      begin method A, 誰執行我: thread 1
      begin method B, 誰執行我: thread 2
      end
      end
    • 嘗試將methodB()也加上synchronized關鍵字,而Thread1仍舊是執行myObject的methodA()方法,Thread2仍舊執行myObject的methodB()方法,結果如下

      • Thread1明明呼叫的是methodA,但是為什麼會使得同個物件的其他synchronized方法受到影響呢?為什麼會使得結果同步?

      • 原因是因為synchronized是一個物件鎖,所以當Thread1取得鎖開始執行methodA時,他實際上其實是取得了myObject這個物件的synchronized

      • 因此當Thread2想要開始執行methodB時,他會去看myObject這個物件的synchronized鎖被取走了沒,而他就會發現已經被Thread1取走了,因此他必須等待Thread1完成後,才能去取得myObject的物件鎖,去執行methodB,所以結果才會是同步的

      class MyObject {
          synchronized public void methodA() { ... }
          synchronized public void methodB() { ... }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 執行myObject物件的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 執行myObject物件的 methodB() 方法
          }
      }
      begin method A, 誰執行我: thread 1
      end
      begin method B, 誰執行我: thread 2
      end
    • 如果把Thread1和Thread2都改成執行methodA方法,不過將methodA方法改成他還會去呼叫methodB方法,注意methodA和methodB方法都有加上synchronized關鍵字,結果如下

      • 可以看到當Thread1搶到myObject物件的鎖執行methodA到一半,當他想要執行methodB時,他就必須去取得methodB的物件鎖,也就是myObject物件鎖,也就是他當初近來methodA時已經拿到的鎖

      • 由於Thread1已經拿到了這個鎖,所以當他再次取得這個物件鎖時,能夠再次取到,這就是鎖重入性

      • 這也說明當一個synchronized方法methodA的內部呼叫本類的其他synchronized方法methodB時,是永遠可以得到鎖的,因為當初進入methodA時此執行緒就已經得過物件鎖了

      class MyObject {
          synchronized public void methodA() {
              System.out.println("begin method A");
              methodB();  //methodA去呼叫同個物件的另一個synchronized方法methodB
              System.out.println("end");
          }
          synchronized public void methodB() {
              System.out.println("begin method B");
              System.out.println("end");
          }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 執行myObject物件的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodA(); //thread 2 執行myObject物件的 methodA() 方法
          }
      }
      begin method A, 誰執行我: thread 1
      begin method B, 誰執行我: thread 1
      end
      end
      begin method A, 誰執行我: thread 2
      begin method B, 誰執行我: thread 2
      end
      end
  • 根據以上例項,小結synchronized關鍵字的特性

    • synchronized是一個物件鎖

      • 假設thread1執行緒已經持有某object物件的synchronized lock

      • 那麼thread2仍然可以呼叫此object物件中的非同步的方法,即是thread2可以直接呼叫那些沒有加上sychronized關鍵字的方法,不需要等待

      • 但是thread2如果呼叫了加上synchronized關鍵字的方法,就必須要等thread1釋放lock才行

    • synchronized物件鎖具有鎖重入性

      • 當一個執行緒取得某object物件的物件鎖時,再次請求此物件鎖是可以再次得到該物件鎖的

      • 也就是說,當一個synchronized方法methodA的內部呼叫本類的其他synchronized方法methodB時,是永遠可以得到鎖的,因為當初進入methodA時此執行緒就已經得過物件鎖了

    • 當執行緒在執行synchronized方法時出現異常,物件鎖會自動釋放

    • 同步不具有繼承性

      • 假設Parent類中有一個synchronized public void hello()方法,那麼繼承到Child類中時此方法會變成public void hello()

      • 如果Child也想要讓此方法變成同步方法,那麼必須手動加上synchronized關鍵字才行

  • 具體例項二

    • 到剛才為止,我們都是將synchronized修飾在方法上,但是直接修飾方法可能會使得critical section範圍設定過大,而真正要同步的區塊其實沒有這麼大,因此synchronized還支援修飾程式碼塊,或是修飾一個變數

      • 使用synchronized (this)可以同步某一段程式碼塊,而這個方式也是一個物件鎖,此例就是會鎖住myObject

      • 要注意只有在synchronized (this)裡的程式碼是同步的,其他地方都是非同步的

        • 所以當thread1執行到methodA的運作很久的方法時,因為他沒有被synchronized修飾,所以是非同步的,所以他不會去管什麼鎖,直接就能執行了

        • 但是當thread1執行到synchronized (this)時,他會想要去取得myObject的物件鎖,而他會發現已經被thread2的methodB取走了,所以他要等thread2釋放物件鎖之後,才能開始執行synchronized (this)裡面的程式碼塊

      class MyObject {
          public void methodA() {
              System.out.println("method A 運作很久的一個方法"); //這是非同步的
              synchronized (this) {
                  System.out.println("begin method A");
                  System.out.println("end");
              }
          }
          synchronized public void methodB() {
              System.out.println("begin method B");
              System.out.println("end");
          }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 執行myObject物件的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 執行myObject物件的 methodB() 方法
          }
      }
      method A 運作很久的一個方法
      begin method B
      end
      begin method A
      end
    • 接續剛才的例子,但是將sychronized改成用來修飾一個變數,methodA去同步myObject物件裡的變數x,methodB去同步myObject物件裡的變數y

      • 可以看到結果是不同步的,原因是因為當synchonized修飾變數時,他的鎖會變成這個變數物件的鎖,而不是myObject這個物件的鎖了

      • 因此methodA取得的是myObject中的x的這個物件鎖,而methodB取得的是myObject中的y的物件鎖,因為他們拿的是兩個不同的鎖,所以他們才不會被同步

      class MyObject {
          private String x = new String();
          private String y = new String();
      ​
          public void methodA() {
              synchronized (x) {
                  System.out.println("begin method A");
                  System.out.println("end");
              }
          }
          public void methodB() {
              synchronized (y) {
                  System.out.println("begin method B");
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 執行myObject物件的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 執行myObject物件的 methodB() 方法
          }
      }
      begin method A
      begin method B
      end
      end
    • 如果再新增一個myObject2,讓thread1執行myObject的methodA,thread2執行myObject2的methodA方法,結果如下

      • 結果會是不同步的,原因是因為synchronized是一個物件鎖,thread1取得的是myObject中的x的物件鎖,thread2取得的是myObject2中的x物件鎖,是兩個鎖,所以才會不同步

      class MyObject {
          private String x = new String();
      ​
          public void methodA() {
              synchronized (x) {
                  System.out.println("begin method A, 誰執行我: " + Thread.currentThread().getName());
                  System.out.println("end");
              }
          }
          public void methodB() { ... }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 執行 myObject 物件的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodA(); //thread 2 執行 myObject2 物件的 methodA() 方法
          }
      }
      begin method A, 誰執行我: thread 1
      end
      begin method A, 誰執行我: thread 2
      end
    • 如果將myObject2刪了,改回Thread1和Thread2都使用myObject這個物件,並且改回Thread1執行methodA,Thread2執行methodB,然後methodA去同步一個變數物件x,但methodB去同步一個類物件myObject,結果如下

      • 結果會是不同步的,是因為他們其實取的鎖是不同的鎖,methodA取得的是myObject中的變數鎖x,而methodB是取得myObject鎖

      • 所以可以知道,就算取得了類物件myObject的鎖,也不會影響其中變數x的鎖,也就是說並非取得物件鎖myObject就會把所有他的變數鎖x也都全部鎖起來,他們之間是獨立的兩個鎖,分別是myObjectx in myObject

      • 所以假設又來了一個新的物件myObject2,那麼鎖就有4把,分別是myObjectx in myObjectmyObject2x in myObject2

      class MyObject {
          private String x = new String();
      ​
          public void methodA() {
              synchronized (x) {
                  System.out.println("begin method A");
                  System.out.println("end");
              }
          }
          public void methodB() {
              synchronized (this) {
                  System.out.println("begin method B");
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 執行myObject物件的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 執行myObject物件的 methodB() 方法
          }
      }
      begin method A
      begin method B
      end
      end
    • 使用synchronized修飾String變數時,要小心jvm中的String常量池特性

      • jvm中的String常量池快取會導致String指向同個物件,像是a、b預期中應該是兩個"1"物件,但是因為String常量池的快取影響,所以a、b儲存的是同個物件,才導致a == b 輸出是true

        public class Test {
            public static void main() {
                String a = "1";
                String b = "1";
                System.out.println(a == b); //輸出true
            }
        }
      • 將methodA改成使用synchornized修飾傳進來的input物件,只有當傳進來的物件一樣,那麼他們取得的鎖才會是同個物件鎖,由於因為String常量池的關係,Thread1和Thread2傳進來的"xx"會被視為視同個物件,所以才會使得結果是同步的

        class MyObject {
            public void methodA(String input) {
                synchronized (input) {
                    System.out.println("begin method A, 誰執行我: " + Thread.currentThread().getName());
                    System.out.println("end");
                }
            }
            public void methodB() { ... }
        }
        ​
        class Thread1 extends Thread {
            public void run(){
                object.methodA("xx"); //thread 1 執行myObject物件的 methodA() 方法
            }
        }
        ​
        class Thread2 extends Thread {
            public void run(){
                object.methodA("xx"); //thread 2 執行myObject物件的 methodA() 方法
            }
        }
        begin method A, 誰執行我: thread 1
        end
        begin method A, 誰執行我: thread 2
        end
      • 如果傳進來的物件改成別的不是String型別的物件,那麼就不會有因為快取而帶來的這個問題,因此Thread1和Thread2傳進來的物件不會是同一個物件,因此他們取得的鎖也不是同把鎖,所以才會使得結果是不同步的

        class MyObject {
            public void methodA(MyType type) {
                synchronized (type) {
                    System.out.println("begin method A, 誰執行我: " + Thread.currentThread().getName());
                    System.out.println("end");
                }
            }
            public void methodB() { ... }
        }
        ​
        class Thread1 extends Thread {
            public void run(){
                object.methodA(new MyType());
            }
        }
        ​
        class Thread2 extends Thread {
            public void run(){
                object.methodA(new MyType());
            }
        }
        begin method A, 誰執行我: thread 1
        begin method A, 誰執行我: thread 2
        end
        end
    • synchronized除了修飾方法、變數、程式碼塊,也可以用來修飾靜態方法static,如果synchronized加在static方法上,那就是加上了一個class鎖,和使用synchronized(MyObject.class)是一樣的,都是要去取得class鎖

      • 執行結果是同步的,原因就是因為Thread1和Thread2都想要去取得MyObject的class鎖,所以才會使得結果同步

      class MyObject {
          synchronized public static void methodA() {
              System.out.println("begin static method A");
              System.out.println("end");
          }
          public void methodB() {
              synchronized (MyObject.class) {
                  System.out.println("begin method B");
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          @Override
          public void run() {
              MyObject.methodA(); //thread 1 執行 MyObject.methodA() 靜態方法
          }
      }
      ​
      class Thread2 extends Thread {
          @Override
          public void run() {
              object.methodB(); //thread 2 執行myObject物件的 methodB() 方法
          }
      }
      begin static method A
      end
      begin method B
      end
    • 假設新增一個物件myObject2,並且改成Thread1執行myObject的methodB,Thread2執行myObject2的methodB,結果如下

      • 結果會是同步的,原因是因為class鎖可以對所有物件的起作用,也就是所有這個類的物件們myObject、myObject2...只有一份class鎖MyObject.class,因此不管生成再多物件,他們使用的都是同一個class鎖,所以結果才會是同步的

      class MyObject {
          synchronized public static void methodA() { ... }
          public void methodB() {
              synchronized (MyObject.class) {
                  System.out.println("begin method B, 誰執行我: " + Thread.currentThread().getName());
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          @Override
          public void run() {
              object.methodA(); //thread 1 執行 myObject 物件的 methodB() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          @Override
          public void run() {
              object.methodB(); //thread 2 執行 myObject2 物件的 methodB() 方法
          }
      }
      begin method B, 誰執行我: thread 1
      end
      begin method B, 誰執行我: thread 2
      end
    • 不過要注意,class鎖、myObject物件鎖、變數x物件鎖,他們彼此之間都是互相獨立的,所以取得了class鎖不代表可以暢行無阻myObject物件鎖和變數x物件鎖

      • 把剛剛的myObject2刪掉,讓Thread1執行myObject的methodA,Thread2執行myObject的methodB,Thread3執行myObject的methodC

      • 結果是不同步的是因為methodA、methodB、methodC各自用了3種不同的鎖

        • methodA是靜態方法,所以他使用的是class鎖

        • methodB使用的是myObject物件鎖

        • methodC使用的是變數x in myObject物件鎖

      class MyObject {
          private String x = new String();
      ​
          synchronized public static void methodA() {
              System.out.println("begin static method A");
              System.out.println("end");
          }
          synchronized public void methodB() {
              System.out.println("begin method B");
              System.out.println("end");
          }
          public void methodC() {
              synchronized (x) {
                  System.out.println("begin method C");
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          @Override
          public void run() {
              MyObject.methodA(); //thread 1 執行 MyObject.methodA() 靜態方法
          }
      }
      ​
      class Thread2 extends Thread {
          @Override
          public void run() {
              object.methodB(); //thread 2 執行myObject物件的 methodB() 方法
          }
      }
      ​
      class Thread3 extends Thread {
          @Override
          public void run() {
              object.methodC(); //thread 3 執行myObject物件的 methodC() 方法
          }
      }
      begin static method A
      begin method B
      begin method C
      end
      end
      end
  • 根據以上例項,總結synchronized關鍵字的特性

    • synchronized物件鎖具有鎖重入性

      • 也就是當一個執行緒取得某object物件的物件鎖時,再次請求此物件鎖是可以再次得到該物件鎖的

    • 當執行緒在執行synchronized方法時出現異常,物件鎖會自動釋放

    • 同步不具有繼承性

    • synchronized不僅可以用在方法上,也能用在變數、程式碼塊、靜態方法上

      • class鎖、myObject物件鎖、x變數 in myObject的物件鎖、y變數 in myObject的物件鎖,彼此間都是獨立的,並非取得myObject物件鎖就能夠取得所有他其中的變數的物件鎖,也並非取得class鎖就能取得所有物件myObject、myObject2...的物件鎖

      • class鎖可以對所有物件的起作用,也就是所有這個類的對像們myObject、myObject2...只有一份class鎖

    • 使用synchronized在String變數上時,要注意String的常量池特性

    • 就算改了某個物件中的某個屬性,像是myObject.setInfo("123),只要myObject物件不改變,那麼synchronized執行的結果仍然會是同步的