大牛聊Java並發編程原理之 線程的互斥與協作機制

可能在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

Tags: