interrupt()中斷對LockSupport.park()的影響

原理簡單講解

首先聲明,本文不會去貼native方法的cpp實現,而是以僞代碼的形式來理解這些native方法。

  • Thread對象的native實現裏有一個成員代表線程的中斷狀態,我們可以認爲它是一個bool型的變量。初始爲false。
  • Thread對象的native實現裏有一個成員代表線程是否可以阻塞的許可permit,我們可以認爲它是一個int型的變量,但它的值只能爲0或1。當爲1時,再累加也會維持1。初始爲0。

調用park()與unpark()

park/unpark實現的僞代碼

下面將以僞代碼的實現來說明park/unpark的實現。

park() {
    if(permit > 0) {
        permit = 0;
        return;
    }

    if(中斷狀態 == true) {
        return;
    }

    阻塞當前線程;  // 將來會從這裏被喚醒

    if(permit > 0) {
        permit = 0;
    }
}

可見,只要permit爲1或者中斷狀態爲true,那麼執行park就不能夠阻塞線程。park只可能消耗掉permit,但不會去消耗掉中斷狀態

unpark(Thread thread) {
    if(permit < 1) {
        permit = 1;
        if(thread處於阻塞狀態)
            喚醒線程thread;
    }
}

unpark一定會將permit置爲1,如果線程阻塞,再將其喚醒。從實現可見,無論調用幾次unparkpermit只能爲1。

park/unpark的實驗

public class test3 {
    public static void main(String[] args) throws InterruptedException {
        LockSupport.park();  //因爲此時permit爲0且中斷狀態爲false,所以阻塞
    }
}

上面程序執行後,程序不會運行結束,main線程阻塞。
原因是,線程默認的permit是0,中斷狀態爲false,所以會阻塞當前線程;

public class test3 {
    public static void main(String[] args) throws InterruptedException {
        LockSupport.unpark(Thread.currentThread());  //置permit爲1
        LockSupport.park();  //消耗掉permit後,直接返回了
    }
}

上面程序執行後,程序運行結束。
原因是LockSupport.unpark(Thread.currentThread())執行後,會使得main線程的permit爲1。而park時發現這個permit爲1時,就會消耗掉這個permit,然後直接返回,所以main線程沒有阻塞。

public class test3 {
    public static void main(String[] args) throws InterruptedException {
        LockSupport.unpark(Thread.currentThread());
        LockSupport.park();  //消耗掉permit後,直接返回了
        LockSupport.park();  //此時permit爲0,中斷狀態爲false,必然會阻塞
    }
}

上面程序執行後,程序不會運行結束,main線程阻塞。
原因是第二次park時,permit爲0了,中斷狀態爲false,所以會阻塞當前線程;

public class test3 {
    public static void main(String[] args){
        Thread main = Thread.currentThread();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子線程開始睡覺");
                try {
                    Thread.sleep(1000);//睡一下保證是在main線程park後,纔去unpark main線程
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName()+"拋出了中斷異常");
                }
                System.out.println("子線程睡醒了,開始unpark main線程");
                LockSupport.unpark(main);
            }
        }).start();

        LockSupport.park();  //此時permit爲0,中斷狀態爲false,必然會阻塞
        //被子線程unpark後,從上一句被喚醒,繼續執行。此時permit還是爲0,中斷狀態爲true。
        LockSupport.park();  //此時permit爲0,中斷狀態爲false,必然會阻塞
    }
}

上面程序執行後,程序不會運行結束,main線程阻塞。
這個程序同上,只是之前的版本都是先unpark,再park。現在保證是,main線程先park後,再去unpark main線程。

interrupt()與park()

interrupt()實現的僞代碼

interrupt(){
    if(中斷狀態 == false) {
        中斷狀態 = true;
    }
    unpark(this);    //注意這是Thread的成員方法,所以我們可以通過this獲得Thread對象
}

interrupt()會設置中斷狀態爲true。注意,interrupt()還會去調用unpark的,所以也會把permit置爲1的。

interrupt()實驗

public class test3 {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().interrupt();
        LockSupport.park();  //消耗掉permit後,直接返回了
    }
}

上面程序執行後,程序運行結束。因爲park執行時permit爲1,直接返回了。

public class test3 {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().interrupt();
        LockSupport.park();  //消耗掉permit後,直接返回了
        LockSupport.park();  //因爲中斷狀態 == true,直接返回了
        LockSupport.park();  //同上
    }
}

上面程序執行後,程序運行結束。馬上無論怎麼park都無法阻塞線程了,因爲此時線程的中斷狀態爲true,函數直接返回了。

public class test3 {
    public static void main(String[] args) throws InterruptedException {
        Thread main = Thread.currentThread();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("馬上開始睡覺");
                try {
                    Thread.sleep(1000);//睡一下保證是在main線程阻塞後,纔去中斷main線程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("睡醒了,開始中斷main線程");
                main.interrupt();
            }
        }).start();

        LockSupport.park();  //此時permit爲0,中斷狀態爲false,必然會阻塞
        //被子線程中斷後,從上一句被喚醒,繼續執行。此時permit爲0,中斷狀態爲true。
        LockSupport.park();  //因爲中斷狀態 == true,直接返回了
        LockSupport.park();  //同上
    }
}

上面程序執行後,程序運行結束。
這個程序同上,只是之前的版本都是先中斷,再park。現在保證是,main線程先阻塞後,再去中斷main線程。

sleep()與interrupt()

sleep()實現的僞代碼

sleep(){//這裏我忽略了參數,假設參數是大於0的即可
    if(中斷狀態 == true) {
        中斷狀態 = false;
        throw new InterruptedException();
    }
    
    線程開始睡覺;   

    if(中斷狀態 == true) {
        中斷狀態 = false;
        throw new InterruptedException();
    }
}

sleep()會去檢測中斷狀態,如果檢測到了,那就消耗掉中斷狀態後,拋出中斷異常。但sleep()不會去動permit

sleep()實驗

public class test3 {
    public static void main(String[] args){
        Thread.currentThread().interrupt();
        try {
            Thread.sleep(1000);  // 消耗掉中斷狀態後,拋出異常
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面程序執行後,拋出異常,程序運行結束。

public class test3 {
    public static void main(String[] args){
        Thread.currentThread().interrupt();
        try {
            Thread.sleep(1000);  // 消耗掉中斷狀態後,拋出異常
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.park();  //消耗掉permit
    }
}

上面程序執行後,拋出異常,程序運行結束。

public class test3 {
    public static void main(String[] args){
        Thread.currentThread().interrupt();
        try {
            Thread.sleep(1000);//消耗掉中斷狀態
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.park();  //消耗掉permit
        LockSupport.park();  //因爲此時permit爲0且中斷狀態爲false,所以阻塞
    }
}

上面程序執行後,拋出異常,程序不會運行結束。

public class test3 {
    public static void main(String[] args){
        Thread main = Thread.currentThread();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子線程開始睡覺");
                try {
                    Thread.sleep(3000);//睡一下保證是在main線程sleep後,纔去中斷main線程
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName()+"拋出了中斷異常");
                }
                System.out.println("子線程睡醒了,開始中斷main線程");
                main.interrupt();
            }
        }).start();

        try {
            System.out.println("主線程開始睡覺");
            Thread.sleep(5000); //main線程開始睡覺
            // 當被中斷喚醒後,會消耗掉中斷狀態。喚醒後繼續執行
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"拋出了中斷異常");
        }
        LockSupport.park();  //消耗掉permit後,直接返回了
        LockSupport.park();  //此時permit爲0,中斷狀態爲false,必然會阻塞
    }
}

上面程序執行後,拋出異常,程序不會運行結束。
這個程序同上,只是之前的版本都是先中斷,再sleep。現在保證是,main線程先sleep後,再去中斷main線程。

wait/join 效果同sleep

public class test3 {
    public static void main(String[] args){
        Thread.currentThread().interrupt();
        Object lock = new Object();
        synchronized (lock) {
            try {
                lock.wait();  //消耗掉中斷狀態
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        LockSupport.park();  //消耗掉permit
        LockSupport.park();  //此時permit爲0,中斷狀態爲false,必然會阻塞
    }
}

上面程序執行後,拋出異常,程序不會運行結束。

public class test3 {
    public static void main(String[] args){
        Thread.currentThread().interrupt();
        Thread thread = new Thread(()->{
            while (true) {}
        });
        thread.start();
        try {
            thread.join();  //消耗掉中斷狀態
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.park();  //消耗掉permit
        LockSupport.park();  //此時permit爲0,中斷狀態爲false,必然會阻塞
    }
}

上面程序執行後,拋出異常,程序不會運行結束。通過Dump Threads後,可以發現main處於WAITING (parking)狀態,即阻塞狀態。

總結

  • park調用後一定會消耗掉permit,無論unpark操作先做還是後做。
  • 如果中斷狀態爲true,那麼park無法阻塞。
  • unpark會使得permit爲1,並喚醒處於阻塞的線程。
  • interrupt()會使得中斷狀態爲true,並調用unpark
  • sleep() / wait() / join()調用後一定會消耗掉中斷狀態,無論interrupt()操作先做還是後做。

關於這一點,“如果中斷狀態爲true,那麼park無法阻塞”。在AQS源碼裏的acquireQueued裏,由於acquireQueued是阻塞式的搶鎖,線程可能重複着 阻塞->被喚醒 的過程,所以在這個過程中,如果遇到了中斷,一定要用Thread.interrupted()中斷狀態消耗掉,並將這個中斷狀態暫時保存到一個局部變量中去。不然只要遇到中斷一次後,線程在搶鎖失敗後卻無法阻塞了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章