天天看點

JAVA多線程系列之LockSupport基本使用特點源碼解析對比Wait & Notify 優勢解析總結面試題

基本使用

// 暫停目前線程
LockSupport.park(); 
// 恢複某個線程的運作
LockSupport.unpark(暫停線程對象)      

特點

與 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必須配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以線程為機關來【阻塞】和【喚醒】線程,而 notify 隻能随機喚醒一個等待線程,notifyAll 是喚醒所有等待線程,就不那麼【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

先Park後Unpark

package com.gzczy.concurrent.week3;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * @Description Park和Unpark
 * @Author chenzhengyu
 * @Date 2020-11-05 19:15
 */
@Slf4j(topic = "c.ParkAndUnParkDemo")
public class ParkAndUnParkDemo {

    public static void main(String[] args) {
//        demo1();
        demo2();
    }

    public static void demo1(){
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        },"t1");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
    //先Unpark再Park
    public static void demo2(){
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}
      

源碼解析

為線程排程目的禁用目前線程,除非許可可用。

如果許可證是可用的,那麼它将被消耗,調用将立即傳回。否則,目前線程會因為線程排程的目的而被暫停,并處于休眠狀态,直到以下三種情況之一發生:

  • 其他一些線程以目前線程為目标調用unpark
  • 其他線程打斷目前線程
  • 不符合邏輯的調用

park方法不會告知你是哪一種情況的發生而導緻的。調用者應該重新檢查導緻線程停在第一個位置的條件。調用者也可以确定,例如,線程傳回時的中斷狀态。

/**
     * Disables the current thread for thread scheduling purposes unless the
     * permit is available.
     *
     * <p>If the permit is available then it is consumed and the call returns
     * immediately; otherwise
     * the current thread becomes disabled for thread scheduling
     * purposes and lies dormant until one of three things happens:
     *
     * <ul>
     * <li>Some other thread invokes {@link #unpark unpark} with the
     * current thread as the target; or
     *
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     *
     * <li>The call spuriously (that is, for no reason) returns.
     * </ul>
     *
     * <p>This method does <em>not</em> report which of these caused the
     * method to return. Callers should re-check the conditions which caused
     * the thread to park in the first place. Callers may also determine,
     * for example, the interrupt status of the thread upon return.
     *
     * @param blocker the synchronization object responsible for this
     *        thread parking
     * @since 1.6
     */
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }      

unpark通過傳入目前線程進行取消park

/**
     * 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);
    }      

對比Wait & Notify 優勢

LockSupport的主要作用就是讓線程進入阻塞等待和喚醒狀态

我們常見的三種線程喚醒的方法

  • 方式1:  使用Object中的wait()方法讓線程等待, 使用Object中的notify()方法喚醒線程
  • 方式2:  使用JUC包中Condition的await()方法讓線程等待,使用signal()方法喚醒線程 
  • 方式3:  LockSupport類可以阻塞目前線程以及喚醒指定被阻塞的線程

Object類中的wait和notify方法實作線程等待和喚醒

package com.gzczy.concurrent.heima.b.wait;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @Description wait notify 測試阻塞
 * @Author chenzhengyu
 * @Date 2021年01月31日 14:12:08
 */
@Slf4j(topic = "c.WaitNotifyDemo")
public class WaitNotifyDemo2 {

    final static Object obj = new Object();

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

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj) {
                log.debug("執行....");
                try {
                    obj.wait(); // 讓線程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代碼....");
            }
        },"t1").start();
        new Thread(() -> {
            synchronized (obj) {
                log.debug("執行....");
                obj.notify(); // 讓線程在obj上一直等待下去
                log.debug("其它代碼....");
            }
        },"t2").start();
    }
}      

異常1:wait方法和notify方法,兩個都去掉同步代碼塊,抛出

IllegalMonitorStateException

JAVA多線程系列之LockSupport基本使用特點源碼解析對比Wait &amp; Notify 優勢解析總結面試題

異常2:将notify放在wait方法前面,程式将無法執行,無法進行喚醒

JAVA多線程系列之LockSupport基本使用特點源碼解析對比Wait &amp; Notify 優勢解析總結面試題

總結:wait和notify方法必須要在同步代碼塊中或者方法裡面成對進行出現才能使用,而且必須先wait後notify之後才OK

通過Park和Unpark進行實作線程等待和喚醒

package com.gzczy.concurrent.heima.b.wait;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * @Description LockSupport Demo 進行線程阻塞
 * @Author chenzhengyu
 * @Date 2021-01-31 14:22
 */
@Slf4j(topic = "c.LockSupportDemo")
public class LockSupportDemo {

    public static void main(String[] args) throws InterruptedException {
        //預設是permit 0
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("執行....是否打斷--->"+Thread.currentThread().isInterrupted());
            // 調用一次park就會消費permit 由剛剛的1變為0 你繼續運作下去吧
            LockSupport.park();
            log.debug("其它代碼,是否打斷--->"+Thread.currentThread().isInterrupted());
            //再次打住 目前沒有許可了 許可為0 那就不放你繼續運作啦
            LockSupport.park();
            log.debug("再次park,是否打斷--->"+Thread.currentThread().isInterrupted());
            //被打斷後會發現無法park住,打斷标記已經為true 線程已經被打斷了
            LockSupport.park();
        }, "t1");
        t1.start();
        new Thread(() -> {
            log.debug("執行....");
            //調用一次unpark就加1變成1,線程還在運作 沒毛病老鐵 你繼續運作吧
            LockSupport.unpark(t1);
            log.debug("其它代碼....");
        }, "t2").start();
        try {
            TimeUnit.SECONDS.sleep(4);
            log.debug("打斷t1線程....");
            t1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}      

運作結果

JAVA多線程系列之LockSupport基本使用特點源碼解析對比Wait &amp; Notify 優勢解析總結面試題

優勢對比

  • 以前的等待喚醒通知機制必須synchronized裡面有一個wait和notify;Lock裡面有await和signal。LockSupport不用持有鎖塊,不用加鎖,程式性能好
  • 先後順序,不容易導緻卡死,虛假喚醒。通過許可證進行授權 

解析

每個線程都有自己的一個 Parker 對象(底層C代碼實作),由三部分組成 _counter , _cond 和 _mutex 打個比喻

  • 線程就像一個旅人,Parker 就像他随身攜帶的背包,條件變量就好比背包中的帳篷。_counter 就好比背包中的備用幹糧(0 為耗盡,1 為充足)
  • 調用 park 就是要看需不需要停下來歇息
    • 如果備用幹糧耗盡,那麼鑽進帳篷歇息
    • 如果備用幹糧充足,那麼不需停留,繼續前進
  • 調用 unpark,就好比令幹糧充足
    • 如果這時線程還在帳篷,就喚醒讓他繼續前進
    • 如果這時線程還在運作,那麼下次他調用 park 時,僅是消耗掉備用幹糧,不需停留繼續前進
  • 因為背包空間有限,多次調用 unpark 僅會補充一份備用幹糧

目前線程調用UnSafe.park

JAVA多線程系列之LockSupport基本使用特點源碼解析對比Wait &amp; Notify 優勢解析總結面試題
  1. 檢查 _counter ,本情況為 0,這時,獲得 _mutex 互斥鎖
  2. 線程進入 _cond 條件變量阻塞
  3. 設定 _counter = 0

目前線程調用UnSafe.unpark(許可為0)

JAVA多線程系列之LockSupport基本使用特點源碼解析對比Wait &amp; Notify 優勢解析總結面試題
  1. 調用 Unsafe.unpark(Thread_0) 方法,設定 _counter 為 1
  2. 喚醒 _cond 條件變量中的 Thread_0
  3. Thread_0 恢複運作
  4. 設定 _counter 為 0

目前線程調用UnSafe.unpark(許可為1)

JAVA多線程系列之LockSupport基本使用特點源碼解析對比Wait &amp; Notify 優勢解析總結面試題

1. 調用 Unsafe.unpark(Thread_0) 方法,設定 _counter 為 1

2. 目前線程調用 Unsafe.park() 方法

3. 檢查 _counter ,本情況為 1,這時線程無需阻塞,繼續運作

4. 設定 _counter 為 0

總結

LockSupport是用來建立鎖和其他同步類的基本線程阻塞原語,它使用了一種名為Permit(許可)的概念來做到阻塞和喚醒線程的功能,每個線程都有一個許可(Permit),Permit隻有兩個值:1和0,預設是0。我們可以把許可看成是一種信号量(Semaphore),但是與Semaphore不同的是,許可的累加上限是1

LockSupport是一個線程阻塞工具,所有的方法都是靜态方法,可以讓線程在任意位置進行阻塞。阻塞之後也有對應的喚醒方法,歸根結底,LockSupport調用的是UnSafe裡面的native代碼

LockSupport提供了Park和Unpark的方法實作阻塞線程和解除線程阻塞的過程

LockSupport和每個使用它的線程都有一個許可(Permit)進行關聯,Permit相當于1,0的開關,預設是0

調用一次unpark就加1變成1,調用一次park就會消費permit,也就是将1變成0的這個過程,同時将park立即傳回

如果再次調用park會變成阻塞(因為Permit為0,是以會阻塞在這裡,直至Permit變為1),如果這個時候調用unpark會把permit設定為1。每個線程都有一個相關的Permit,Permit最多隻有1個,重複調用unpark也不會積累憑證

面試題

為什麼可以先喚醒線程後阻塞線程?

答:因為UnPark獲得了一個憑證,之後調用Park方法,就可以名正言順的憑證消費,故不會阻塞

為什麼喚醒兩次後阻塞兩次,但是最終的結果還是會阻塞線程?

答:因為憑證的數量最多為1,連續調用兩次unpark和調用一次unpark的效果是一樣的,隻會增加一個憑證;而調用兩次park卻需要消費兩個憑證,證不夠,不能放行