JUC工具(LockSupport)

糯米๓發表於2024-04-26

LockSupport用來建立鎖和其他同步類的基本執行緒阻塞

LockSupport用來建立鎖和其他同步類的基本執行緒阻塞原語。簡而言之,當呼叫LockSupport.park時,表示當前執行緒將會等待,直至獲得許可,當呼叫LockSupport.unpark時,必須把等待獲得許可的執行緒作為引數進行傳遞,好讓此執行緒繼續執行

LockSupport介紹

LockSupport是JDK中比較底層的類,用來建立鎖和其他同步工具類的基本執行緒阻塞原語。java鎖和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是透過呼叫 LockSupport .park()和 LockSupport .unpark()實現執行緒的阻塞和解除阻塞的。LockSupport中的park() 和 unpark() 的作用分別是阻塞執行緒和解除阻塞執行緒,而且park()和unpark()不會遇到“Thread.suspend 和 Thread.resume所可能引發的死鎖”問題。

LockSupport類是Java6(JSR166-JUC)引入的一個類,提供了基本的執行緒同步原語。LockSupport實際上是呼叫了Unsafe類裡的函式,歸結到Unsafe裡,只有兩個函式:

  • park:阻塞當前執行緒(Block current thread),字面理解park,就算佔住,停車的時候不就把這個車位給佔住了麼?起這個名字還是很形象的。
  • unpark: 使給定的執行緒停止阻塞(Unblock the given thread blocked )。

因為park() 和 unpark()有許可的存在;呼叫 park() 的執行緒和另一個試圖將其 unpark() 的執行緒之間的競爭將保持活性。

private static void setBlocker(Thread t, Object arg)
// 返回提供給最近一次尚未解除阻塞的 park 方法呼叫的 blocker 物件,如果該呼叫不受阻塞,則返回 null。
static Object getBlocker(Thread t)
// 為了執行緒排程,禁用當前執行緒,除非許可可用。
static void park()
// 為了執行緒排程,在許可可用之前禁用當前執行緒。
static void park(Object blocker)
// 為了執行緒排程禁用當前執行緒,最多等待指定的等待時間,除非許可可用。
static void parkNanos(long nanos)
// 為了執行緒排程,在許可可用前禁用當前執行緒,並最多等待指定的等待時間。
static void parkNanos(Object blocker, long nanos)
// 為了執行緒排程,在指定的時限前禁用當前執行緒,除非許可可用。
static void parkUntil(long deadline)
// 為了執行緒排程,在指定的時限前禁用當前執行緒,除非許可可用。
static void parkUntil(Object blocker, long deadline)
// 如果給定執行緒的許可尚不可用,則使其可用。
static void unpark(Thread thread)

兩個重點

(1)操作物件

歸根結底,LockSupport呼叫的Unsafe中的native程式碼:

public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

    public static void park() {
        UNSAFE.park(false, 0L);
    }

兩個函式宣告清楚地說明了操作物件:park函式是將當前Thread阻塞,而unpark函式則是將指定執行緒Thread喚醒

與Object類的wait/notify機制相比,park/unpark有兩個優點:

  1. 以thread為操作物件更符合阻塞執行緒的直觀定義;
  2. 操作更精準,可以準確地喚醒某一個執行緒(notify隨機喚醒一個執行緒,notifyAll喚醒所有等待的執行緒),增加了靈活性。

(2)關於許可

在上面的文字中,我使用了阻塞和喚醒,是為了和wait/notify做對比。

  • 其實park/unpark的設計原理核心是“許可”。park是等待一個許可。unpark是為某執行緒提供一個許可。如果某執行緒A呼叫park,那麼除非另外一個執行緒呼叫unpark(A)給A一個許可,否則執行緒A將阻塞在park操作上。
  • 有一點比較難理解的,是unpark操作可以再park操作之前。也就是說,先提供許可。當某執行緒呼叫park時,已經有許可了,它就消費這個許可,然後可以繼續執行。這其實是必須的。考慮最簡單的生產者(Producer)消費者(Consumer)模型:Consumer需要消費一個資源,於是呼叫park操作等待;Producer則生產資源,然後呼叫unpark給予Consumer使用的許可。非常有可能的一種情況是,Producer先生產,這時候Consumer可能還沒有構造好(比如執行緒還沒啟動,或者還沒切換到該執行緒)。那麼等Consumer準備好要消費時,顯然這時候資源已經生產好了,可以直接用,那麼park操作當然可以直接執行下去。如果沒有這個語義,那將非常難以操作。
  • 但是這個“許可”是不能疊加的,“許可”是一次性的。比如執行緒B連續呼叫了三次unpark函式,當執行緒A呼叫park函式就使用掉這個“許可”,如果執行緒A再次呼叫park,則進入等待狀態。

park和unpark的靈活之處

上面已經提到,unpark函式可以先於park呼叫,這個正是它們的靈活之處。

一個執行緒它有可能在別的執行緒unPark之前,或者之後,或者同時呼叫了park,那麼因為park的特性,它可以不用擔心自己的park的時序問題,否則,如果park必須要在unpark之前,那麼給程式設計帶來很大的麻煩!!

考慮一下,兩個執行緒同步,要如何處理?

在Java5裡是用wait/notify/notifyAll來同步的。wait/notify機制有個很蛋疼的地方是,比如執行緒B要用notify通知執行緒A,那麼執行緒B要確保執行緒A已經在wait呼叫上等待了,否則執行緒A可能永遠都在等待。 程式設計的時候就會很蛋疼。

另外,是呼叫notify,還是notifyAll?

notify只會喚醒一個執行緒,如果錯誤地有兩個執行緒在同一個物件上wait等待,那麼又悲劇了。為了安全起見,貌似只能呼叫notifyAll了。而unpark可以指定到特定執行緒。

park/unpark模型真正解耦了執行緒之間的同步,執行緒之間不再需要一個Object或者其它變數來儲存狀態,不再需要關心對方的狀態。

程式碼示例

LockSupport 很類似於二元訊號量(只有1個許可證可供使用),如果這個許可還沒有被佔用,當前執行緒獲取許可並繼 續 執行;如果許可已經被佔用,當前線 程阻塞,等待獲取許可。

public static void main(String[] args)
{
     LockSupport.park();
     System.out.println("block.");
}

執行該程式碼,可以發現主執行緒一直處於阻塞狀態。因為 許可預設是被佔用的 ,呼叫park()時獲取不到許可,所以進入阻塞狀態。

如下程式碼:先釋放許可,再獲取許可,主執行緒能夠正常終止。LockSupport許可的獲取和釋放,一般來說是對應的,如果多次unpark,只有一次park也不會出現什麼問題,結果是許可處於可用狀態。

arduino複製程式碼public static void main(String[] args)
{
     Thread thread = Thread.currentThread();
     LockSupport.unpark(thread);//釋放許可
     LockSupport.park();// 獲取許可
     System.out.println("b");
}

LockSupport是可不重入 的,如果一個執行緒連續2次呼叫 LockSupport.park(),那麼該執行緒一定會一直阻塞下去。

public static void main(String[] args) throws Exception
{
    Thread thread = Thread.currentThread();
    
    LockSupport.unpark(thread);
    
    System.out.println("a");
    LockSupport.park();
    System.out.println("b");
    LockSupport.park();
    System.out.println("c");
}

這段程式碼列印出a和b,不會列印c,因為第二次呼叫park的時候,執行緒無法獲取許可出現死鎖。

下面我們來看下LockSupport對應中斷的響應性

public static void t2() throws Exception
{
    Thread t = new Thread(new Runnable()
    {
        private int count = 0;

        @Override
        public void run()
        {
            long start = System.currentTimeMillis();
            long end = 0;

            while ((end - start) <= 1000)
            {
                count++;
                end = System.currentTimeMillis();
            }

            System.out.println("after 1 second.count=" + count);

            //等待或許許可
            LockSupport.park();
            System.out.println("thread over." + Thread.currentThread().isInterrupted());

        }
    });

    t.start();

    Thread.sleep(2000);

    // 中斷執行緒
    t.interrupt();

    System.out.println("main over");
}

最終執行緒會列印出thread over.true。這說明 執行緒如果因為呼叫park而阻塞的話,能夠響應中斷請求(中斷狀態被設定成true),但是不會丟擲InterruptedException

總結

Thread.sleep()和Object.wait()的區別

首先,我們先來看看Thread.sleep()和Object.wait()的區別,這是一個爛大街的題目了,大家應該都能說上來兩點。

  • Thread.sleep()不會釋放佔有的鎖,Object.wait()會釋放佔有的鎖;
  • Thread.sleep()必須傳入時間,Object.wait()可傳可不傳,不傳表示一直阻塞下去;
  • Thread.sleep()到時間了會自動喚醒,然後繼續執行;
  • Object.wait()不帶時間的,需要另一個執行緒使用Object.notify()喚醒;
  • Object.wait()帶時間的,假如沒有被notify,到時間了會自動喚醒,這時又分好兩種情況,一是立即獲取到了鎖,執行緒自然會繼續執行;二是沒有立即獲取鎖,執行緒進入同步佇列等待獲取鎖;

其實,他們倆最大的區別就是Thread.sleep()不會釋放鎖資源,Object.wait()會釋放鎖資源。

Object.wait()和Condition.await()的區別

Object.wait()和Condition.await()的原理是基本一致的,不同的是Condition.await()底層是呼叫LockSupport.park()來實現阻塞當前執行緒的。

實際上,它在阻塞當前執行緒之前還幹了兩件事,一是把當前執行緒新增到條件佇列中,二是“完全”釋放鎖,也就是讓state狀態變數變為0,然後才是呼叫LockSupport.park()阻塞當前執行緒。

Thread.sleep()和LockSupport.park()的區別

LockSupport.park()還有幾個兄弟方法——parkNanos()、parkUtil()等,我們這裡說的park()方法統稱這一類方法。

  • 從功能上來說,Thread.sleep()和LockSupport.park()方法類似,都是阻塞當前執行緒的執行,且都不會釋放當前執行緒佔有的鎖資源;
  • Thread.sleep()沒法從外部喚醒,只能自己醒過來;
  • LockSupport.park()方法可以被另一個執行緒呼叫LockSupport.unpark()方法喚醒;
  • Thread.sleep()方法宣告上丟擲了InterruptedException中斷異常,所以呼叫者需要捕獲這個異常或者再丟擲;
  • LockSupport.park()方法不需要捕獲中斷異常;
  • Thread.sleep()本身就是一個native方法;
  • LockSupport.park()底層是呼叫的Unsafe的native方法;

Object.wait()和LockSupport.park()的區別

二者都會阻塞當前執行緒的執行,他們有什麼區別呢? 經過上面的分析相信你一定很清楚了,真的嗎? 往下看!

  • Object.wait()方法需要在synchronized塊中執行;
  • LockSupport.park()可以在任意地方執行;
  • Object.wait()方法宣告丟擲了中斷異常,呼叫者需要捕獲或者再丟擲;
  • LockSupport.park()不需要捕獲中斷異常;
  • Object.wait()不帶超時的,需要另一個執行緒執行notify()來喚醒,但不一定繼續執行後續內容;
  • LockSupport.park()不帶超時的,需要另一個執行緒執行unpark()來喚醒,一定會繼續執行後續內容;

park()/unpark()底層的原理是“二元訊號量”,你可以把它相像成只有一個許可證的Semaphore,只不過這個訊號量在重複執行unpark()的時候也不會再增加許可證,最多隻有一個許可證。

如果在wait()之前執行了notify()會怎樣?

如果當前的執行緒不是此物件鎖的所有者,卻呼叫該物件的notify()或wait()方法時丟擲IllegalMonitorStateException異常;

如果當前執行緒是此物件鎖的所有者,wait()將一直阻塞,因為後續將沒有其它notify()喚醒它。

如果在park()之前執行了unpark()會怎樣?

執行緒不會被阻塞,直接跳過park(),繼續執行後續內容

LockSupport.park()會釋放鎖資源嗎?

不會,它只負責阻塞當前執行緒,釋放鎖資源實際上是在Condition的await()方法中實現的

相關文章