1. 程式人生 > >Thread.sleep、Object.wait、LockSupport.park 區別

Thread.sleep、Object.wait、LockSupport.park 區別

文章目錄


在java語言中,可以通過3種方式讓執行緒進入休眠狀態,分別是 Thread.sleep()Object.wait()LockSupport.park()方法。這三種方法的表現和原理都各有不同,今天稍微研究了下這幾個方法的區別。

Thread.sleep() 方法

Thread.sleep(time)方法必須傳入指定的時間,執行緒將進入休眠狀態,通過jstack輸出執行緒快照的話此時該執行緒的狀態應該是TIMED_WAITING,表示休眠一段時間。

另外,該方法會丟擲InterruptedException異常,這是受檢查異常,呼叫者必須處理。

通過sleep方法進入休眠的執行緒不會釋放持有的鎖,因此,在持有鎖的時候呼叫該方法需要謹慎。

Object.wait() 方法

我們都知道,java的每個物件都隱式的繼承了Object類。因此每個類都有自己的wait()方法。我們通過object.wait()方法也可以讓執行緒進入休眠。wait()有3個過載方法:

public final void wait() throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;

如果不傳timeout,wait將會進入無限制的休眠當中,直到有人喚醒他。使用wait()讓執行緒進入休眠的話,無論有沒有傳入timeout引數,執行緒的狀態都將是WAITING狀態。

另外,必須獲得物件上的鎖後,才可以執行該物件的wait方法。否則程式會在執行時丟擲IllegalMonitorStateException

異常。

Object waitObject = new Object();
try {
    //沒獲取到waitObject的鎖,呼叫該方法丟擲IllegalMonitorStateException異常
      waitObject.wait();
} catch (InterruptedException e) {
      e.printStackTrace();
}

//正確的呼叫方式  
Object waitObject = new Object();
try {
    //先獲取到waitObject的鎖
    synchronized (waitObject){
        waitObject.wait();
    }
} catch (InterruptedException e) {
      e.printStackTrace();
}

再呼叫wait()方法後,執行緒進入休眠的同時,會釋放持有的該物件的鎖,這樣其他執行緒就能在這期間獲取到鎖了。

呼叫Object物件的notify()或者notifyAll()方法可以喚醒因為wait()而進入等待的執行緒。

LockSupport.park() 方法

通過LockSupport.park()方法,我們也可以讓執行緒進入休眠。它的底層也是呼叫了Unsafe類的park方法

//Unsafe.java類
//喚醒指定的執行緒
public native void unpark(Thread jthread);
//isAbsolute表示後面的時間是絕對時間還是相對時間,time表示時間,time=0表示無限阻塞下去
public native void park(boolean isAbsolute, long time);

呼叫park方法時,還允許設定一個blocker物件,主要用來給監視工具和診斷工具確定執行緒受阻塞的原因。

呼叫park方法進入休眠後,執行緒狀態為WAITING

實現原理

LockSupport.park() 的實現原理是通過二元訊號量做的阻塞,要注意的是,這個訊號量最多隻能加到1。我們也可以理解成獲取釋放許可證的場景。unpark()方法會釋放一個許可證,park()方法則是獲取許可證,如果當前沒有許可證,則進入休眠狀態,知道許可證被釋放了才被喚醒。無論執行多少次unpark()方法,也最多隻會有一個許可證。

和wait的不同

park、unpark方法和wait、notify()方法有一些相似的地方。都是休眠,然後喚醒。但是wait、notify方法有一個不好的地方,就是我們在程式設計的時候必須能保證wait方法比notify方法先執行。如果notify方法比wait方法晚執行的話,就會導致因wait方法進入休眠的執行緒接收不到喚醒通知的問題。而park、unpark則不會有這個問題,我們可以先呼叫unpark方法釋放一個許可證,這樣後面執行緒呼叫park方法時,發現已經許可證了,就可以直接獲取許可證而不用進入休眠狀態了。

另外,和wait方法不同,執行park進入休眠後並不會釋放持有的鎖

對中斷的處理

park方法不會丟擲InterruptedException,但是它也會響應中斷。當外部執行緒對阻塞執行緒呼叫interrupt方法時,park阻塞的執行緒也會立刻返回。

Thread parkThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("park begin");

                //等待獲取許可
                LockSupport.park();
                //輸出thread over.true
                System.out.println("thread over." + Thread.currentThread().isInterrupted());

            }
        });
        parkThread.start();

        Thread.sleep(2000);
        // 中斷執行緒
        parkThread.interrupt();

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

上面的demo最終會輸出

park begin
main over
thread over.true

說明因park進入休眠的執行緒收到中斷通知後也會立刻返回,並且可以手動通過Thread.currentThread().isInterrupted()獲取到中斷位。

總結

題外話:關於java程序的關閉

在linux中,我們通常用kill命令來關閉一個程序。眾所周知,kill有-9和-15兩種引數,預設是-15。如果是-15引數,系統就傳送一個關閉訊號給程序,然後等待程序關閉。在這個過程中,目標程序可以釋放手中的資源,以及進行一些關閉操作。

正是有了這個概念,我曾經很大一段時間對java程序的關閉流程有所誤解。在我原先的理解中,java程序接收到關閉訊號後,會逐一給阻塞中的程序傳送中斷訊號,並等待執行緒處理完。但其實這是錯誤的

java程序收到關閉訊號後,不會去關心執行中的那些執行緒是否執行完,也不會給阻塞中的執行緒傳送中斷訊號。我們只能通過繫結關閉鉤子來中斷目標執行緒並等待執行緒執行完。

final Thread waitThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread begin");

                //等待獲取許可
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //輸出thread over.true
                System.out.println("thread over." + Thread.currentThread().isInterrupted());

                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        waitThread.start();
        //繫結鉤子
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    waitThread.interrupt();
                    waitThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("shutdown success");
            }
        }));

java程序在收到關閉訊號後,會執行所有綁定了shutdownHook的執行緒,確保這些繫結的執行緒都執行完了才真正關閉。因此,我們要釋放資源就要在shutdownHook的執行緒內操作,然後線上程內等待其他釋放資源的執行緒執行完成。

注意,所有綁定了shutdownHook的執行緒也是並行執行的,不是順序執行。另外,用-9引數的kill不會等shutdownHook執行緒執行完就退出。