1. 程式人生 > 實用技巧 >JUC 包下工具類,它的名字叫 LockSupport !你造麼?

JUC 包下工具類,它的名字叫 LockSupport !你造麼?

前言

LockSupport 是 JUC 中常用的一個工具類,主要作用是掛起和喚醒執行緒。在閱讀 JUC 原始碼中經常看到,所以很有必要了解一下。

公眾號:liuzhihangs ,記錄工作學習中的技術、開發及原始碼筆記;時不時分享一些生活中的見聞感悟。歡迎大佬來指導!

介紹

基本執行緒阻塞原語建立鎖和其他同步類。Basic thread blocking primitives for creating locks and other synchronization classes.

LockSupport 類每個使用它的執行緒關聯一個許可(在意義上的Semaphore類)。 如果許可可用,呼叫 park 將立即返回,並在此過程中消費它; 否則可能阻塞。如果許可不是可用,可以呼叫 unpark 使得許可可用。(但與Semaphore不同,許可不能累積。最多有一個。)

方法 park 和 unpark 提供了阻塞的有效手段和解鎖執行緒不會遇到死鎖問題,而 Thread.suspend 和 Thread.resume 是不能用於這種目的:因為許可的存在,一個執行緒呼叫 park 和另一個執行緒試圖 unpark 它之間的競爭將保持活性。 此外,如果呼叫者執行緒被中斷,park 將返回,並且支援設定超時。 該 park 方法也可能返回在其他任何時間,“毫無理由”,因此通常必須在一個迴圈中呼叫的返回後重新檢查條件。 在這個意義上park作為“忙碌等待”不會浪費太多的時間自旋的優化,但必須以配對 unpark 使用。

這三種形式的 park 還支援 blocker 物件引數。而執行緒被阻塞時是允許使用監測和診斷工具,以確定執行緒被阻塞的原因。(診斷工具可以使用getBlocker(Thread) 方法 。)同時推薦使用帶有 blocker 引數的 park方法,通常做法是 blocker 被設定為 this 。

上面的意思總結下來個人理解是:

  1. 許可(permit)的上限是1,也就是說只有 0 或 1 。
  2. park: 沒有許可的時候,permit 為 0 ,呼叫 park 會阻塞;有許可的時候,permit 為 1 , 呼叫 park 會扣除一個許可,然後返回。
  3. unpark:沒有許可的時候,permit 為 0 ,呼叫 unpark 會增加一個許可,因為許可上限是 1 , 所以呼叫多次也只會為 1 個。
  4. 執行緒初始的時候是沒有許可的。
  5. park 的當前執行緒如果被中斷,會立即返回,並不會丟擲中斷異常。
  6. park 方法的呼叫一般要放在一個迴圈判斷體裡面。

大概如圖所示:

下面是原始碼註釋中的案例:

/**
 * FIFO 獨佔鎖
 */
class FIFOMutex {
   private final AtomicBoolean locked = new AtomicBoolean(false);
   private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();

   public void lock() {
     boolean wasInterrupted = false;
     Thread current = Thread.currentThread();
     waiters.add(current);

     // Block while not first in queue or cannot acquire lock
     // 不在佇列頭,或者鎖被佔用,則阻塞, 就是隻有佇列頭的可以獲得鎖
     while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
       LockSupport.park(this);
       if (Thread.interrupted()) // ignore interrupts while waiting
         wasInterrupted = true;
     }

     waiters.remove();
     if (wasInterrupted)          // reassert interrupt status on exit
       current.interrupt();
   }

   public void unlock() {
     locked.set(false);
     LockSupport.unpark(waiters.peek());
   }
 }

驗證

執行緒初始有沒有許可?

public class LockSupportTest {

    public static void main(String[] args) {

        System.out.println("開始執行……");

        LockSupport.park();

        System.out.println("LockSupport park 之後……");

    }
}
  1. 執行後會發現,程式碼在 park 處阻塞。說明,執行緒初始是沒有許可的。

新增許可並消耗許可

public class LockSupportTest {

    public static void main(String[] args) {

        System.out.println("開始執行……");

        LockSupport.unpark(Thread.currentThread());

        System.out.println("執行 - park");
        
        LockSupport.park();

        System.out.println("LockSupport park 之後……");

    }

}
public class LockSupportTest {

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("執行緒 " + Thread.currentThread().getName() + "開始執行 park");
                LockSupport.park(this);
                System.out.println("執行緒 " + Thread.currentThread().getName() + "執行 park 結束");
            }
        });

        thread.start();
        // 保證 上面執行緒先執行,然後再主執行緒
        Thread.sleep(5000);
        System.out.println("開始執行 unpark(thread)");
        LockSupport.unpark(thread);
        Thread.sleep(5000);
        System.out.println("執行 unpark(thread) 結束");

    }

}

通過上面示例可以看出:

  1. 執行 unpark 可以進行給予指定執行緒一個證書。
  2. 執行緒當前被 park 阻塞,此時給予證書之後, park 會消耗證書,然後繼續執行。

許可上限為 1


public class LockSupportTest {

    public static void main(String[] args) {

        System.out.println("unpark 1次");
        LockSupport.unpark(Thread.currentThread());
        System.out.println("unpark 2次");
        LockSupport.unpark(Thread.currentThread());

        System.out.println("執行 - park 1 次");
        LockSupport.park();
        System.out.println("執行 - park 2 次");
        LockSupport.park();
        
        System.out.println("LockSupport park 之後……");

    }

}
  1. 執行緒阻塞,可以看出 permit 只能有一個

中斷可以使 park 繼續執行並不會丟擲異常

public class LockSupportTest {
    public static void main(String[] args)  {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("執行緒 " + Thread.currentThread().getName() + "開始執行 park");
                LockSupport.park(this);
                System.out.println("執行緒 " + Thread.currentThread().getName() + "執行 park 結束");

                System.out.println("執行緒 " + Thread.currentThread().getName() + "開始執行 park 第二次");
                LockSupport.park(this);
                System.out.println("執行緒 " + Thread.currentThread().getName() + "執行 park 第二次 結束");

            }
        });

        try {
            thread.start();
            // 保證 上面執行緒先執行,然後再主執行緒
            Thread.sleep(5000);
            System.out.println("開始執行 unpark(thread)");
            // LockSupport.unpark(thread);
            thread.interrupt();
            Thread.sleep(5000);
            System.out.println("執行 unpark(thread) 結束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

輸出結果:

/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/bin/java ...
執行緒 Thread-0開始執行 park
開始執行 unpark(thread)
執行緒 Thread-0執行 park 結束
執行緒 Thread-0開始執行 park 第二次
執行緒 Thread-0執行 park 第二次 結束
執行 unpark(thread) 結束
  1. 可以看出執行緒中斷,park 會繼續執行,並且沒有丟擲異常。
  2. thread.interrupt(); 呼叫之後, 設定執行緒中斷標示,unpark 沒有清除中斷標示,第二個 park 也會繼續執行。

使用診斷工具

liuzhihang % > jps
76690 LockSupportTest
77130 Jps
liuzhihang % > jstack 77265
...
"main" #1 prio=5 os_prio=31 tid=0x00007f7f3e80a000 nid=0xe03 waiting on condition [0x000070000dfcd000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
        at com.liuzhihang.source.LockSupportTest.main(LockSupportTest.java:14)
  1. 中間省略部分,但是可以看出執行緒進入 WAITING 狀態

原始碼分析


public class LockSupport {

    private static final sun.misc.Unsafe UNSAFE;

    /**
     * 為執行緒 thread 設定一個許可
     * 無許可,則新增一個許可,有許可,則不新增
     * 如果執行緒因為 park 被阻塞, 新增許可之後,會解除阻塞狀態
     */
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

    /**
     * 有許可,則使用該許可
     * 沒有許可,阻塞執行緒,直到獲得許可
     * 傳遞 blocker 是為了方便使用診斷工具
     */
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

    /**
     * 設定執行緒的 blocker 屬性
     */
    private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
}

LockSupport 的 park unpark 方法,實際呼叫的是底層 Unsafe 類的 native 方法。


public final class Unsafe {
    
    public native void unpark(Object var1);

    public native void park(boolean var1, long var2);
}

既然呼叫了 Unsafe 到此處肯定不能善罷甘休。

hotspot 原始碼

這塊是下載的官方包中的原始碼,閱讀並查閱資料瞭解的大概邏輯,不清楚之處,希望指匯出來。

也可以直接跳過直接看結論。

檢視jdk原始碼
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/5a83b7215107/src/share/vm/runtime/park.hpp

這塊在以 os_linux 程式碼為例
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/5a83b7215107/src/os/linux/vm/os_linux.cpp



  1. 在底層維護了一個 _counter 通過更新 _counter 的值來標示是否有證明。
  2. 在 park 時,判斷 _counter 為 0,則阻塞等待,為 1 則獲得更新為 0 並返回。
  3. 在 unpark 時,判斷 _counter 為 0,則給予憑證,並喚醒執行緒,為 1 則直接返回。

總結

總結也是和預想的是相同的。

  1. 許可(permit)的上限是1,也就是說只有 0 或 1 。
  2. park: 沒有許可的時候,permit 為 0 ,呼叫 park 會阻塞;有許可的時候,permit 為 1 , 呼叫 park 會扣除一個許可,然後返回。
  3. unpark:沒有許可的時候,permit 為 0 ,呼叫 unpark 會增加一個許可,因為許可上限是 1 , 所以呼叫多次也只會為 1 個。
  4. 執行緒初始的時候是沒有許可的。
  5. park 的當前執行緒如果被中斷,會立即返回,並不會丟擲中斷異常。

擴充套件

  • park/unpark 和 wait/notify 區別
  1. park 阻塞當前執行緒,unpark 喚醒指定執行緒。
  2. wait() 需要結合鎖使用,並釋放鎖資源,如果沒有設定超時時間,則需要 notify() 喚醒。而 notify() 是隨機喚醒執行緒。