LockSupport:一個很靈活的線程工具類

2021-02-20 愚公要移山

剛剛開頭提到過,LockSupport是一個線程工具類,所有的方法都是靜態方法,可以讓線程在任意位置阻塞,也可以在任意位置喚醒。

它的內部其實兩類主要的方法:park(停車阻塞線程)和unpark(啟動喚醒線程)。

//(1)阻塞當前線程
public static void park(Object blocker); 
//(2)暫停當前線程,有超時時間
public static void parkNanos(Object blocker, long nanos); 
//(3)暫停當前線程,直到某個時間
public static void parkUntil(Object blocker, long deadline); 
//(4)無期限暫停當前線程
public static void park(); 
//(5)暫停當前線程,不過有超時時間的限制
public static void parkNanos(long nanos); 
//(6)暫停當前線程,直到某個時間
public static void parkUntil(long deadline);  
//(7)恢復當前線程
public static void unpark(Thread thread); 
public static Object getBlocker(Thread t);

注意上面的123方法,都有一個blocker,這個blocker是用來記錄線程被阻塞時被誰阻塞的。用於線程監控和分析工具來定位原因的。

現在我們知道了LockSupport是用來阻塞和喚醒線程的,而且之前相信我們都知道wait/notify也是用來阻塞和喚醒線程的,那和它相比,LockSupport有什麼優點呢?

這裡假設你已經了解了wait/notify的機制,如果不了解,可以在網上一搜,很簡單。相信你既然學到了這個LockSupport,相信你已經提前已經學了wait/notify。

我們先來舉一個使用案例:

public class LockSupportTest {
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(getName() + " 進入線程");
            LockSupport.park();
            System.out.println("t1線程運行結束");
        }
    }
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
        System.out.println("t1已經啟動,但是在內部進行了park");
        LockSupport.unpark(t1);
        System.out.println("LockSupport進行了unpark");
    }
}

上面這段代碼的意思是,我們定義一個線程,但是在內部進行了park,因此需要unpark才能喚醒繼續執行,不過上面,我們在MyThread進行的park,在main線程進行的unpark。

這樣來看,好像和wait/notify沒有什麼區別。那他的區別到底是什麼呢?這個就需要仔細的觀察了。這裡主要有兩點:

(1)wait和notify都是Object中的方法,在調用這兩個方法前必須先獲得鎖對象,但是park不需要獲取某個對象的鎖就可以鎖住線程。

(2)notify只能隨機選擇一個線程喚醒,無法喚醒指定的線程,unpark卻可以喚醒一個指定的線程。

區別就是這倆,還是主要從park和unpark的角度來解釋的。既然這個LockSupport這麼強,我們就深入一下他的源碼看看。


    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

blocker是用來記錄線程被阻塞時被誰阻塞的。用於線程監控和分析工具來定位原因的。setBlocker(t, blocker)方法的作用是記錄t線程是被broker阻塞的。因此我們只關注最核心的方法,也就是UNSAFE.park(false, 0L)。

UNSAFE是一個非常強大的類,他的的操作是基於底層的,也就是可以直接操作內存,因此我們從JVM的角度來分析一下:

每個java線程都有一個Parker實例:

class Parker : public os::PlatformParker {
private:
  volatile int _counter ;
  ...
public:
  void park(bool isAbsolute, jlong time);
  void unpark();
  ...
}
class PlatformParker : public CHeapObj<mtInternal> {
  protected:
    pthread_mutex_t _mutex [1] ;
    pthread_cond_t  _cond  [1] ;
    ...
}

我們換一種角度來理解一下park和unpark,可以想一下,unpark其實就相當於一個許可,告訴特定線程你可以停車,特定線程想要park停車的時候一看到有許可,就可以立馬停車繼續運行了。因此其執行順序可以顛倒。

現在有了這個概念,我們體會一下上面JVM層面park的方法,這裡面counter欄位,就是用來記錄所謂的「許可」的。

本小部分總結來源於:https://www.jianshu.com/p/1f16b838ccd8

當調用park時,先嘗試直接能否直接拿到「許可」,即_counter>0時,如果成功,則把_counter設置為0,並返回。

void Parker::park(bool isAbsolute, jlong time) {
  // Ideally we'd do something useful while spinning, such
  // as calling unpackTime().
  // Optional fast-path check:
  // Return immediately if a permit is available.
  // We depend on Atomic::xchg() having full barrier semantics
  // since we are doing a lock-free update to _counter.
  if (Atomic::xchg(0, &_counter) > 0) return;

如果不成功,則構造一個ThreadBlockInVM,然後檢查_counter是不是>0,如果是,則把_counter設置為0,unlock mutex並返回:

  ThreadBlockInVM tbivm(jt);
  // no wait needed
  if (_counter > 0)  { 
    _counter = 0;
    status = pthread_mutex_unlock(_mutex);

否則,再判斷等待的時間,然後再調用pthread_cond_wait函數等待,如果等待返回,則把_counter設置為0,unlock mutex並返回:

if (time == 0) {  
  status = pthread_cond_wait (_cond, _mutex) ;  
}  
_counter = 0 ;  
status = pthread_mutex_unlock(_mutex) ;  
assert_status(status == 0, status, "invariant") ;  
OrderAccess::fence();  

這就是整個park的過程,總結來說就是消耗「許可」的過程。

還是先來看一下JDK源碼:

    /**
     * Makes available the permit for the given thread, if it
     * was not already available.  If the thread was blocked on
     * {@code park} then it will unblock.  Otherwise, its next call
     * to {@code park} is guaranteed not to block. This operation
     * is not guaranteed to have any effect at all if the given
     * thread has not been started.
     *
     * @param thread the thread to unpark, or {@code null}, in which case
     *        this operation has no effect
     */
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

上面注釋的意思是給線程生產許可證。

當unpark時,則簡單多了,直接設置_counter為1,再unlock mutext返回。如果_counter之前的值是0,則還要調用pthread_cond_signal喚醒在park中等待的線程:

void Parker::unpark() {  
  int s, status ;  
  status = pthread_mutex_lock(_mutex);  
  assert (status == 0, "invariant") ;  
  s = _counter;  
  _counter = 1;  
  if (s < 1) {  
     if (WorkAroundNPTLTimedWaitHang) {  
        status = pthread_cond_signal (_cond) ;  
        assert (status == 0, "invariant") ;  
        status = pthread_mutex_unlock(_mutex);  
        assert (status == 0, "invariant") ;  
     } else {  
        status = pthread_mutex_unlock(_mutex);  
        assert (status == 0, "invariant") ;  
        status = pthread_cond_signal (_cond) ;  
        assert (status == 0, "invariant") ;  
     }  
  } else {  
    pthread_mutex_unlock(_mutex);  
    assert (status == 0, "invariant") ;  
  }  
}  

ok,現在我們已經對源碼進行了分析,整個過程其實就是生產許可和消費許可的過程。而且這個生產過程可以反過來。也就是先生產再消費。下面我們使用幾個例子驗證一波。

public class LockSupportTest {
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(getName() + " 進入線程");
            LockSupport.park();
            System.out.println(" 運行結束");
            System.out.println("是否中斷:" + Thread.currentThread().isInterrupted());
        }
    }
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
        System.out.println("t1線程已經啟動了,但是在內部LockSupport進行了park");
        t1.interrupt();
        System.out.println("main線程結束");
    }
}

我們看一下結果:


public static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + " 進入線程");
            LockSupport.park();
            System.out.println(" 運行結束");
        }
    }

我們只需在park之前先休眠1秒鐘,這樣可以確保unpark先執行。

OK,今天的文章先寫到這,如有問題,還請批評指正。

相關焦點

  • Java中的多線程你只要看這一篇就夠了
    Thread類中的yield方法可以讓一個running狀態的線程轉入runnable。內功心法:每個對象都有的方法(機制)synchronized, wait, notify 是任何對象都具有的同步工具。讓我們先來了解他們
  • 【高並發】ReadWriteLock怎麼和緩存扯上關係了?!
    總體來說,讀寫鎖需要遵循以下原則:一個共享變量在同一時刻只能被一個寫線程進行寫操作。一個共享變量在被寫線程執行寫操作時,此時這個共享變量不能被讀線程執行讀操作。這裡,需要小夥伴們注意的是:讀寫鎖和互斥鎖的一個重要的區別就是:讀寫鎖允許多個線程同時讀共享變量,而互斥鎖不允許。所以,在高並發場景下,讀寫鎖的性能要高於互斥鎖。
  • 吹爆系列:探索 Android 多線程的一切
    從 Activity 的角度來看,用戶點擊按鈕後打開一個 Activity,就相當於是觸發了 Activity 的 onCreate() 方法。從線程的角度來看,開發者調用了 start() 方法,就相當於是觸發了 Thread 的 run() 方法。
  • Java8線程池ThreadPoolExecutor底層原理及其源碼解析
    避免頻繁創建、銷毀線程的開銷; 復用創建的線程.及時響應提交的任務; 提交一個任務,不再是每次都需要創建新的線程.避免每次提交的任務都新建線程, 造成伺服器資源耗盡, 線程頻繁上下文切換等伺服器資源開銷.更容易監控、管理線程; 可以統計出已完成的任務數, 活躍的線程數, 等待的任務數等, 可以重寫hook方法beforeExecute, afterExecute
  • java多線程中sleep和wait的4個區別,你知道幾個?
    sleep和wait的區別是面試中一個非常常見的問題,因為從表象來看,好像sleep和wait都能使線程處於阻塞狀態,但是卻有著本質上的卻別。這篇文章就來好好分析一下。整體的區別其實是有四個:1、sleep是線程中的方法,但是wait是Object中的方法。
  • Java中的並發工具類CountDownLatch
    Java中的並發工具類在多線程編程的時候,有時候需要控制並發流,Java本身提供了幾個控制並發的工具類,比如CountDownLatch,CyclicBarrier,Semaphore1、CountDownLatch允許一個或者多個線程等等其他線程完成。如果有個會議,等所有的人到了才能開始,假如每個人都是一個線程,開會需要等待每個線程結束。
  • 鴻蒙內核源碼分析:Task/線程管理篇
    線程可以使用或等待CPU、使用內存空間等系統資源,並獨立於其它線程運行。鴻蒙內核每個進程內的線程獨立運行、獨立調度,當前進程內線程的調度不受其它進程內線程的影響。鴻蒙內核中的線程採用搶佔式調度機制,同時支持時間片輪轉調度和FIFO調度方式。鴻蒙內核的線程一共有32個優先級(0-31),最高優先級為0,最低優先級為31。
  • 線程不是你想中斷就能中斷
    為什麼不強制停止 如何用 interrupt 停止線程 sleep 期間能否感受到中斷 停止線程的方式有幾種 總結啟動線程需要調用 Thread 類的 start() 方法,並在 run() 方法中定義需要執行的任務。啟動一個線程非常簡單,但如果想要正確停止它就沒那麼容易了。
  • 線程數設置多少更合適?
    IO 密集型任務:比如像 MySQL 資料庫、文件的讀寫、網絡通信等任務,這類任務不會特別消耗 CPU 資源,但是 IO 操作比較耗時,會佔用比較多時間。在知道如何判斷任務的類別後,讓我們分兩個場景進行討論:CPU 密集型任務對於 CPU 密集型計算,多線程本質上是提升多核 CPU 的利用率,所以對於一個 8 核的 CPU,每個核一個線程,理論上創建 8 個線程就可以了。
  • Java項目實踐,CountDownLatch實現多線程閉鎖
    摘要本文主要介紹Java多線程並發中閉鎖(Latch)的基本概念、原理、示例代碼、應用場景,通過學習,可以掌握多線程並發時閉鎖(Latch)的使用方法。概念「閉鎖」就是指一個被鎖住了的門將線程a擋在了門外(等待執行),只有當門打開後(其他線程執行完畢),門上的鎖才會被打開,a才能夠繼續執行。
  • 最全面的Java多線程用法解析
    最全面的java多線程用法解析,如果你對Java的多線程機制並沒有深入的研究,那麼本文可以幫助你更透徹地理解Java多線程的原理以及使用方法。1.創建線程在Java中創建線程有兩種方法:使用Thread類和使用Runnable接口。在使用Runnable接口時需要建立一個Thread實例。
  • 三萬字總結最全Java線程池源碼面試題
    原理示意圖4 線程池API4.1 接口定義和實現類繼承關係圖可以認為ScheduledThreadPoolExecutor是最豐富的實現類!4.2.2 Executors工具類可以自己實例化線程池,也可用Executors創建線程池的工廠類,常用方法ExecutorService 的抽象類AbstractExecutorService提供了submit、invokeAll 等方法的實現,但是核心方法Executor.execute
  • Java並發工具三劍客之CountDownLatch源碼解析
    CountDownLatch是Java並發包下的一個工具類,latch是門閂的意思,顧名思義,CountDownLatch就是有一個門閂擋住了裡面的人(線程)出來,當count減到0的時候,門閂就打開了,人(線程)就可以出來了。
  • Lockup or lockdown? 禁閉
    Reader question:Coronavirus lockup or coronavirus lockdown, which is correct?My comments:Both are correct.
  • Android-skin-support 換膚原理全面解析
    star 數比較多的換膚框架 -Android-skin-support 。簡單了解之後,可以快速上手,並且侵入性很低.作為一名合格的程式設計師,當然需要了解其背後的原理才能算是真正的靈活運用。並且有bug的話,也能很快定位是哪裡的問題,這對於公司的項目後期維護是非常有用的。
  • python GIL與多線程是什麼關係呢?
    複製代碼我們可以將上述操作畫一個圖,便於你更直觀地理解:這裡,x 指向一個列表,列表的第一個元素為 1;執行了 append 操作後,第二個元素又反過來指向 x,即指向了 x 所指向的列表,因此形成了一個無限嵌套的循環:[1, [1, [1, [1, …]]]]。
  • Android Support Library 23.2 發布啦!
    關於Android Support Library 23.2的一些令人期待的新feature,一經發出即在圈內引爆,從國外發酵到國內,微博微信上大家也在熱烈討論此事,視頻隨後被設置為private狀態無法閱讀,發布者表示這次是意外洩漏,正式發布應該在本周內,大家可以圍觀這個原討論:https://www.reddit.com/r/androiddev/comments/4790vn/android_support_library
  • 又是如何中斷線程?
    OopMap存儲兩種對象引用:1、對象內的引用在類加載完的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來。2、主動式中斷當GC需要中斷線程時,設置一個標誌,各個線程去輪詢這個標誌,發現需要中斷,線程就自己中斷。輪詢點和安全點在一個地方,在加上創建對象需要分配內存的地方。