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執行緒執行完就退出。