阻塞和喚醒線程——LockSupport功能簡介及原理淺析

1.LockSupport功能簡介

在java併發包下各類同步組件的底層實現中,LockSupport的身影到處可見。JDK中的定義爲用來建立鎖和其餘同步類的線程阻塞原語。併發

*Basic thread blocking primitives for creating locks and other
*synchronization classes.

咱們可使用它來阻塞和喚醒線程,功能和wait,notify有些類似,可是LockSupport比起wait,notify功能更強大,也好用的多。ide

1.1 使用wait,notify阻塞喚醒線程

  • 例子
public class WaitNotifyTest {
    private static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(new WaitThread()).start();
        new Thread(new NotifyThread()).start();
    }
    static class WaitThread implements Runnable {
        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("start wait!");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("end wait!");
            }
        }
    }
    static class NotifyThread implements Runnable {
        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("start notify!");
                obj.notify();
                System.out.println("end notify");
            }
        }
    }
}
  • 結果

使用wait,notify來實現等待喚醒功能至少有兩個缺點:工具

  • 1.由上面的例子可知,wait和notify都是Object中的方法,在調用這兩個方法前必須先得到鎖對象,這限制了其使用場合:只能在同步代碼塊中。
  • 2.另外一個缺點可能上面的例子不太明顯,當對象的等待隊列中有多個線程時,notify只能隨機選擇一個線程喚醒,沒法喚醒指定的線程。

而使用LockSupport的話,咱們能夠在任何場合使線程阻塞,同時也能夠指定要喚醒的線程,至關的方便。線程

1.2 使用LockSupport阻塞喚醒線程

  • 例子
public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        System.out.println("開始線程喚醒");
        LockSupport.unpark(parkThread);
        System.out.println("結束線程喚醒");

    }

    static class ParkThread implements Runnable{

        @Override
        public void run() {
            System.out.println("開始線程阻塞");
            LockSupport.park();
            System.out.println("結束線程阻塞");
        }
    }
}
  • 結果

LockSupport.park();能夠用來阻塞當前線程,park是停車的意思,把運行的線程比做行駛的車輛,線程阻塞則至關於汽車停車,至關直觀。該方法還有個變體LockSupport.park(Object blocker),指定線程阻塞的對象blocker,該對象主要用來排查問題。方法LockSupport.unpark(Thread thread)用來喚醒線程,由於須要線程做參數,因此能夠指定線程進行喚醒。code

2. LockSupport的其餘特點

2.1 能夠先喚醒線程再阻塞線程

在阻塞線程前睡眠1秒中,使喚醒動做先於阻塞發生,看看會發生什麼orm

  • 例子
public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        System.out.println("開始線程喚醒");
        LockSupport.unpark(parkThread);
        System.out.println("結束線程喚醒");

    }

    static class ParkThread implements Runnable{

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("開始線程阻塞");
            LockSupport.park();
            System.out.println("結束線程阻塞");
        }
    }
}
  • 結果

先喚醒指定線程,而後阻塞該線程,可是線程並無真正被阻塞而是正常執行完後退出了。這是怎麼回事?咱們試着在改動下代碼,先喚醒線程兩次,在阻塞線程兩次,看看會發生什麼。對象

2.2 先喚醒線程兩次再阻塞兩次會發生什麼

  • 例子
public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        for(int i=0;i<2;i++){
            System.out.println("開始線程喚醒");
            LockSupport.unpark(parkThread);
            System.out.println("結束線程喚醒");
        }
    }

    static class ParkThread implements Runnable{

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<2;i++){
                System.out.println("開始線程阻塞");
                LockSupport.park();
                System.out.println("結束線程阻塞");
            }
        }
    }
}
  • 結果

能夠看到線程被阻塞致使程序一直沒法結束掉。對比上面的例子,咱們能夠得出一個匪夷所思的結論,先喚醒線程,在阻塞線程,線程不會真的阻塞;可是先喚醒線程兩次再阻塞兩次時就會致使線程真的阻塞。那麼這究竟是爲何?blog

3. LockSupport阻塞和喚醒線程原理淺析

既然是淺析,那就不摳底層細節,只講關鍵,細節可能有疏漏和不到位的地方。
每一個線程都有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] ;
    ...
}

LockSupport就是經過控制變量_counter來對線程阻塞喚醒進行控制的。原理有點相似於信號量機制。

  • 當調用park()方法時,會將_counter置爲0,同時判斷前值,小於1說明前面被unpark過,則直接退出,不然將使該線程阻塞。
  • 當調用unpark()方法時,會將_counter置爲1,同時判斷前值,小於1會進行線程喚醒,不然直接退出。
    形象的理解,線程阻塞須要消耗憑證(permit),這個憑證最多隻有1個。當調用park方法時,若是有憑證,則會直接消耗掉這個憑證而後正常退出;可是若是沒有憑證,就必須阻塞等待憑證可用;而unpark則相反,它會增長一個憑證,但憑證最多隻能有1個。
  • 爲何能夠先喚醒線程後阻塞線程?
    由於unpark得到了一個憑證,以後調用park由於有憑證消費,故不會阻塞。
  • 爲何喚醒兩次後阻塞兩次會阻塞線程。
    由於憑證的數量最多爲1,連續調用兩次unpark和調用一次unpark效果同樣,只會增長一個憑證;而調用兩次park卻須要消費兩個憑證。

4. 總結

LockSupport是JDK中用來實現線程阻塞和喚醒的工具。使用它能夠在任何場合使線程阻塞,能夠指定任何線程進行喚醒,而且不用擔憂阻塞和喚醒操做的順序,但要注意連續屢次喚醒的效果和一次喚醒是同樣的。 JDK併發包下的鎖和其餘同步工具的底層實現中大量使用了LockSupport進行線程的阻塞和喚醒,掌握它的用法和原理可讓咱們更好的理解鎖和其它同步工具的底層實現。

相關文章
相關標籤/搜索