大牛聊Java併發程式設計原理之 執行緒的互斥與協作機制

程式零世界發表於2020-07-12

可能在synchronized關鍵字的實現原理中,你已經知道了它的底層是使用Monitor的相關指令來實現的,但是還不清楚Monitor的具體細節。本文將讓你徹底Monitor的底層實現原理。

管程

一個管程可以被認為是一個帶有特殊房間的建築,這個特殊房間只能被一個執行緒佔用。這個房間包含很多資料和程式碼。

如果一個執行緒要佔用特殊房間(也就是紅色區域),那麼首先它必須在Hallway中等待。排程器基於某些規則(例如先進先出)從Hallway中取一個執行緒。如果執行緒在Hallway由於某些原因被掛起,它將會被送往等待房間(也就是藍色區域),在一段時間後被排程到特殊房間中。

簡而言之,監視器是一種監視現場訪問特殊房間的裝置。他能夠使有且僅有一個執行緒訪問的受保護的程式碼和資料。

Monitor

在Java虛擬機器中,每一個物件和類都與一個監視器相關聯。為了實現監視器的互斥功能,鎖(有時候也稱為互斥體)與每一個物件和類關聯。在作業系統書中,這叫做訊號量,互斥鎖也被稱為二元訊號量。

如果一個執行緒擁有某些資料上的鎖,其他執行緒想要獲得鎖只能等到這個執行緒釋放鎖。如果我們在進行多執行緒程式設計時總是需要編寫一個訊號量,那就不太方便了。幸運的是,我們不需要這樣做,因為JVM會自動為我們做這件事。

為了宣告一個同步區域(這裡意味著資料不可能被超過一個執行緒訪問),Java提供了synchronized塊和synchronized方法。一旦程式碼被synchronized關鍵字繫結,它就是一個監視器區域。它的鎖將會在後面被JVM實現。

Monitor是 Java中用以實現執行緒之間的互斥與協作的主要手段,它可以看成是物件或者Class的鎖。每一個物件都有,也僅有一個 monitor。下面這個圖,描述了執行緒和 Monitor之間關係,以及執行緒的狀態轉換圖:

進入區(Entrt Set):表示執行緒通過synchronized要求獲取物件的鎖,但並未得到。

擁有者(The Owner):表示執行緒成功競爭到物件鎖。

等待區(Wait Set):表示執行緒通過物件的wait方法,釋放物件的鎖,並在等待區等待被喚醒。

執行緒狀態

  • NEW,未啟動的。不會出現在Dump中。

  • RUNNABLE,在虛擬機器內執行的。

  • BLOCKED,等待獲得監視器鎖。

  • WATING,無限期等待另一個執行緒執行特定操作。

  • TIMED_WATING,有時限的等待另一個執行緒的特定操作。

  • TERMINATED,已退出的。

舉個例子:

package com.jiuyan.mountain.test;

import java.util.concurrent.TimeUnit;

/**
 * Hello world!
 *
 */
public class App {

   public static void main(String[] args) throws InterruptedException {
       MyTask task = new MyTask();
       Thread t1 = new Thread(task);
       t1.setName("t1");
       Thread t2 = new Thread(task);
         t2.setName("t2");
        t1.start();
         t2.start();
  }

}

class MyTask implements Runnable {

   private Integer mutex;

   public MyTask() {
       mutex = 1;
   }

   @Override
   public void run() {
       synchronized (mutex) {
         while(true) {
           System.out.println(Thread.currentThread().getName());
           try {
               TimeUnit.SECONDS.sleep(5);
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
          }
        }
   }

}

執行緒狀態:

"t2" prio=10 tid=0x00007f7b2013a800 nid=0x67fb waiting for monitor entry [0x00007f7b17087000]
 java.lang.Thread.State: BLOCKED (on object monitor)
  at com.jiuyan.mountain.test.MyTask.run(App.java:35)
  - waiting to lock <0x00000007d6b6ddb8> (a java.lang.Integer)
  at java.lang.Thread.run(Thread.java:745)

"t1" prio=10 tid=0x00007f7b20139000 nid=0x67fa waiting on condition [0x00007f7b17188000]
 java.lang.Thread.State: TIMED_WAITING (sleeping)
  at java.lang.Thread.sleep(Native Method)

t1沒有搶到鎖,所以顯示BLOCKED。t2搶到了鎖,但是處於睡眠中,所以顯示TIMED_WAITING,有限等待某個條件來喚醒。

把睡眠的程式碼去掉,執行緒狀態變成了:

"t2" prio=10 tid=0x00007fa0a8102800 nid=0x6a15 waiting for monitor entry [0x00007fa09e37a000]
 java.lang.Thread.State: BLOCKED (on object monitor)
  at com.jiuyan.mountain.test.MyTask.run(App.java:35)
  - waiting to lock <0x0000000784206650> (a java.lang.Integer)
  at java.lang.Thread.run(Thread.java:745)

"t1" prio=10 tid=0x00007fa0a8101000 nid=0x6a14 runnable [0x00007fa09e47b000]
 java.lang.Thread.State: RUNNABLE
  at java.io.FileOutputStream.writeBytes(Native Method)

t1顯示RUNNABLE,說明正在執行,這裡需要額外說明一下,如果這個執行緒正在查詢資料庫,但是資料庫發生死鎖,雖然執行緒顯示在執行,實際上並沒有工作,對於IO型的執行緒別隻用執行緒狀態來判斷工作是否正常。
MyTask的程式碼小改一下,執行緒拿到鎖之後執行wait,釋放鎖,進入等待區。

public void run() {
     synchronized (mutex) {
         if(mutex == 1) {
             try {
                 mutex.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
      }
  }

執行緒狀態如下:

"t2" prio=10 tid=0x00007fc5a8112800 nid=0x5a58 in Object.wait() [0x00007fc59b58c000]
 java.lang.Thread.State: WAITING (on object monitor)
  at java.lang.Object.wait(Native Method)

"t1" prio=10 tid=0x00007fc5a8111000 nid=0x5a57 in Object.wait() [0x00007fc59b68d000]
 java.lang.Thread.State: WAITING (on object monitor)
  at java.lang.Object.wait(Native Method)

兩個執行緒都顯示WAITING,這次是無限期的,需要重新獲得鎖,所以後面跟了on object monitor
再來個死鎖的例子:

package com.jiuyan.mountain.test;

import java.util.concurrent.TimeUnit;

/**
 * Hello world!
 *
 */
public class App {

    public static void main(String[] args) throws InterruptedException {
        MyTask task1 = new MyTask(true);
        MyTask task2 = new MyTask(false);
        Thread t1 = new Thread(task1);
        t1.setName("t1");
        Thread t2 = new Thread(task2);
        t2.setName("t2");
        t1.start();
        t2.start();
    }

}

class MyTask implements Runnable {

    private boolean flag;

    public MyTask(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if(flag) {
            synchronized (Mutex.mutex1) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                synchronized (Mutex.mutex2) {
                    System.out.println("ok");
                }
            }
        } else {
            synchronized (Mutex.mutex2) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                synchronized (Mutex.mutex1) {
                    System.out.println("ok");
                }
            }
        }
    }

}

class Mutex {
   public static Integer mutex1 = 1;
   public static Integer mutex2 = 2;
}  

執行緒狀態:

"t2" prio=10 tid=0x00007f5f9c122800 nid=0x3874 waiting for monitor entry [0x00007f5f67efd000]
 java.lang.Thread.State: BLOCKED (on object monitor)
  at com.jiuyan.mountain.test.MyTask.run(App.java:55)
  - waiting to lock <0x00000007d6c45bd8> (a java.lang.Integer)
  - locked <0x00000007d6c45be8> (a java.lang.Integer)
  at java.lang.Thread.run(Thread.java:745)

"t1" prio=10 tid=0x00007f5f9c121000 nid=0x3873 waiting for monitor entry [0x00007f5f67ffe000]
 java.lang.Thread.State: BLOCKED (on object monitor)
  at com.jiuyan.mountain.test.MyTask.run(App.java:43)
  - waiting to lock <0x00000007d6c45be8> (a java.lang.Integer)
  - locked <0x00000007d6c45bd8> (a java.lang.Integer)
  at java.lang.Thread.run(Thread.java:745)

Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x00007f5f780062c8 (object 0x00000007d6c45bd8, a java.lang.Integer),
which is held by "t1"
"t1":
waiting to lock monitor 0x00007f5f78004ed8 (object 0x00000007d6c45be8, a java.lang.Integer),
which is held by "t2"

這個有點像哲學家就餐問題,每個執行緒都持有對方需要的鎖,那就執行不下去了。

最後

私信回覆 資料 領取一線大廠Java面試題總結+各知識點學習思維導+一份300頁pdf文件的Java核心知識點總結!

這些資料的內容都是面試時面試官必問的知識點,篇章包括了很多知識點,其中包括了有基礎知識、Java集合、JVM、多執行緒併發、spring原理、微服務、Netty 與RPC 、Kafka、日記、設計模式、Java演算法、資料庫、Zookeeper、分散式快取、資料結構等等。

作者:monitor

file

相關文章