前言
本章內(nèi)容涉及wait()、notify()撒遣、notifyAll()邮偎、sleep()、join()义黎、interrupt()和對應(yīng)的超時方法禾进。
Object中的相關(guān)方法介紹
package java.lang;
public class Object {
//喚醒此對象監(jiān)視器等待的單個線程,被喚醒線程進(jìn)入就緒狀態(tài)
public final native void notify();
//喚醒此對象監(jiān)視器等待的所有線程轩缤,被喚醒線程進(jìn)入就緒狀態(tài)
public final native void notifyAll();
//讓當(dāng)前線程進(jìn)入等待(阻塞)狀態(tài)命迈,超時會被喚醒
public final native void wait(long timeout) throws InterruptedException;
//納秒大于零,毫秒數(shù)加1火的。如果有需求壶愤,直接把毫秒數(shù)加1調(diào)用上一個方法即可
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
//讓當(dāng)前線程進(jìn)入無限期等待(阻塞)狀態(tài)
public final void wait() throws InterruptedException {
wait(0);
}
...
}
注釋中已經(jīng)大致說明了方法用途。
wait()馏鹤、notify()
線程的等待與喚醒為什么在Object中而不在線程Thread中征椒,需要強(qiáng)調(diào)的是,這里說的線程等待是指讓線程等待在某一個對象的監(jiān)視器上(用Object.wait()表示)湃累,等待時會釋放持有該對象的同步鎖勃救,依賴于synchronized關(guān)鍵字使用(否則報(bào)監(jiān)視器狀態(tài)異常IllegalMonitorStateException)。同樣治力,線程喚醒也是指喚醒等待在某一個對象監(jiān)視器上的線程(用Object.notify()表示)蒙秒,也依賴于synchronized關(guān)鍵字。說到這里宵统,前面提出的問題已經(jīng)基本回答完了晕讲,說白了,線程的等待與喚醒都是基于某一對象的監(jiān)視器马澈,而線程本身只是其中的一種而已瓢省。代碼示例:
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author HXJ
* @date 2018/7/20
*/
public class WaitNotify {
public static void main(String[] args) {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//用于等待喚醒的對象
final Object waitAndNotify = new Object();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " notify線程開始運(yùn)行!...");
//thread-notify線程在main線程等待釋放同步鎖之前阻塞在waitAndNotify對象的監(jiān)視器上
synchronized (waitAndNotify) {
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 喚醒主線程痊班!");
//喚醒在waitAndNotify對象監(jiān)視器上等待的main線程
waitAndNotify.notify();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 已喚醒主線程勤婚!");
}
}
}, "thread-notify");
synchronized (waitAndNotify) {
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " start notify線程");
thread.start();
try {
//主線程休眠兩秒是為了表現(xiàn)thread-notify線程的阻塞等待
Thread.sleep(2 * 1000);
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 主線程!wait()");
//讓線程等待在waitAndNotify對象的監(jiān)視器上,并釋放同步鎖
waitAndNotify.wait();
} catch (InterruptedException e) {
}
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 主線程涤伐!繼續(xù)運(yùn)行...");
}
}
}
運(yùn)行結(jié)果:
2018-07-20 17:53:06 main start notify線程
2018-07-20 17:53:06 thread-notify notify線程開始運(yùn)行雾鬼!...
2018-07-20 17:53:08 main 主線程!wait()
2018-07-20 17:53:08 thread-notify 喚醒主線程载弄!
2018-07-20 17:53:08 thread-notify 已喚醒主線程懒豹!
2018-07-20 17:53:08 main 主線程!繼續(xù)運(yùn)行...
運(yùn)行結(jié)果說明:
從代碼中可以看出豆村,主線程main先創(chuàng)建了一個用于等待液兽、喚醒的對象waitAndNotify,然后再創(chuàng)建線程thread-notify掌动,線程thread-notify的運(yùn)行邏輯用于喚醒等待線程四啰。接著主線程main獲取waitAndNotify對象同步鎖,啟動線程thread-notify粗恢;由于waitAndNotify同步鎖已經(jīng)被main線程持有柑晒,且休眠兩秒,所以線程thread-notify阻塞等待waitAndNotify對象同步鎖眷射,直到main線程休眠結(jié)束后調(diào)用wait()等待在waitAndNotify對象監(jiān)視器上并釋放同步鎖匙赞;接著線程thread-notify取得同步鎖佛掖,喚醒等待在waitAndNotify監(jiān)視器上的main線程,main線程繼續(xù)運(yùn)行至結(jié)束涌庭。
wait(long timeout)芥被、notifyAll()
前邊介紹完wait()和notify()后,這兩個函數(shù)已經(jīng)很簡單了坐榆,通過Object源碼發(fā)現(xiàn)wait()也是通過調(diào)用wait(long timeout)實(shí)現(xiàn)的拴魄,參數(shù)為0意思是無限期等待,直到被喚醒席镀。如果參數(shù)大于0匹中,假如參數(shù)是500,意思是等待500毫秒豪诲,等待期間如果線程未被喚醒顶捷,則500毫秒后自動喚醒。notify()是喚醒一個等待線程屎篱,而notifyAll()是喚醒所以等待的線程焊切。用法與notify()一致。
Thread中的sleep(long millis)芳室、join()专肪、interrupt()
sleep(long millis)
java.lang.Thread
/**
* 線程休眠,定義拋出中斷異常
*/
public static native void sleep(long millis) throws InterruptedException;
線程休眠堪侯,Thread中的靜態(tài)方法嚎尤,用法比較簡單,上文代碼示例中已經(jīng)出現(xiàn)過伍宦,Thread.sleep(毫秒數(shù))即讓當(dāng)前運(yùn)行的線程進(jìn)入TIMED_WAITING (sleeping)阻塞狀態(tài)芽死,調(diào)用完成,當(dāng)前線程進(jìn)入休眠狀態(tài)次洼,直到休眠設(shè)置的毫秒數(shù)后由系統(tǒng)喚醒关贵。需要注意的是線程與同步鎖沒有關(guān)系,所以不會存在等待釋放同步鎖一說卖毁,它可以隨意的嵌入方法代碼的任何地方進(jìn)行調(diào)用揖曾。上文代碼片段:
try {
//主線程休眠兩秒是為了表現(xiàn)thread-notify線程的阻塞等待
Thread.sleep(2 * 1000);
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 主線程!wait()");
//讓線程等待在waitAndNotify對象的監(jiān)視器上,并釋放同步鎖
waitAndNotify.wait();
} catch (InterruptedException e) {
}
jion()
jion()線程間等待亥啦,實(shí)際上是通過Object.wait()實(shí)現(xiàn)的炭剪。
java.lang.Thread
/**
* 等待這個線程運(yùn)行結(jié)束
*/
public final void join() throws InterruptedException {
join(0);
}
/**
* 大于等于500000納秒或納秒大于0且毫秒等于零時,毫秒加一
*/
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
/**
* 等待指定的毫秒數(shù)后翔脱,如果被等待的線程還沒結(jié)束會超時自動喚醒奴拦,放棄等待
* 注意synchronized關(guān)鍵字,說明獲取的是這個線程實(shí)例對象的同步鎖届吁,等待在這個線程實(shí)例的監(jiān)視器上
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
//參數(shù)為0错妖,如果this線程實(shí)例(非當(dāng)前執(zhí)行調(diào)用線程)還活著绿鸣,則等待無限期等待,直到被等待線程運(yùn)行結(jié)束
while (isAlive()) {
wait(0);
}
} else {
//超時等待暂氯,等待當(dāng)前線程實(shí)例運(yùn)行結(jié)束或超時后系統(tǒng)喚醒
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
從代碼中可以看出來潮模,join()方法是讓調(diào)用它的線程進(jìn)行等待,等待在某一個線程實(shí)例的監(jiān)視器上株旷,說白了就是調(diào)用哪個線程實(shí)例的join()方法再登,就是等待該線程運(yùn)行結(jié)束尔邓,或是等待超時后等待的線程繼續(xù)運(yùn)行晾剖。不知道大家有沒有發(fā)現(xiàn),Thread中的join()方法中的wait()并沒有對應(yīng)的notify()梯嗽,被等待的線程運(yùn)行結(jié)束后是怎么喚醒等待它的線程呢齿尽?其實(shí)線程運(yùn)行結(jié)束退出時,jvm會執(zhí)行退出線程的本地退出exit方法灯节,執(zhí)行退出邏輯循头。Thread.cpp中相應(yīng)代碼:
static void ensure_join(JavaThread* thread) {
// We do not need to grap the Threads_lock, since we are operating on ourself.
Handle threadObj(thread, thread->threadObj());
assert(threadObj.not_null(), "java thread object must exist");
ObjectLocker lock(threadObj, thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
// Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
// Clear the native thread instance - this makes isAlive return false and allows the join()
// to complete once we've done the notify_all below
java_lang_Thread::set_thread(threadObj(), NULL);
//此處為喚醒等待在此線程監(jiān)視器上的所有線程
lock.notify_all(thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}
// 線程退出函數(shù)
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
...
// Notify waiters on thread object. This has to be done after exit() is called
// on the thread (if the thread is the last thread in a daemon ThreadGroup the
// group should have the destroyed bit set before waiters are notified).
//這里從命名可以看出,線程退出確保處理join相關(guān)邏輯
ensure_join(this);
...
// Remove from list of active threads list,
//and notify VM thread if we are the last non-daemon thread
Threads::remove(this);
}
代碼中可以看出炎疆,線程退出邏輯中有喚醒所有等待線程的相關(guān)邏輯卡骂。
主線程等待子線程 代碼示例:
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 2; i++) {
try {
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 運(yùn)行中...");
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 運(yùn)行結(jié)束");
}
}, "thread-sub");
//啟動子線程
thread.start();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 等待子線程運(yùn)行結(jié)束...");
//等待子線程運(yùn)行結(jié)束,即主線程等待在子線程實(shí)例的監(jiān)視器上
thread.join();
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 繼續(xù)運(yùn)行...");
}
}
運(yùn)行結(jié)果:
2018-07-24 16:38:56 main 等待子線程運(yùn)行結(jié)束...
2018-07-24 16:38:56 thread-sub 運(yùn)行中...
2018-07-24 16:38:57 thread-sub 運(yùn)行中...
2018-07-24 16:38:58 thread-sub 運(yùn)行結(jié)束
2018-07-24 16:38:58 main 繼續(xù)運(yùn)行...
從結(jié)果可以看出形入,主線程會等待子線程運(yùn)行結(jié)束后才會繼續(xù)運(yùn)行全跨。從實(shí)例代碼中看,可能你會疑問亿遂,main執(zhí)行子線程thread.join()調(diào)用浓若,為什么進(jìn)行等待的是main而不是thread-sub子線程呢?我們再回頭看join(long millis)源碼蛇数,源碼中isAlive()方法在本示例代碼中是判斷thread-sub子線程如果還沒有運(yùn)行結(jié)束挪钓,在正在運(yùn)行的main主線程調(diào)用wait(0)或wait(delay)進(jìn)行等待。再次強(qiáng)調(diào)耳舅,wait()作用是讓當(dāng)前線程等待碌上,也就是讓main主線程等待在thread-sub子線程對象的監(jiān)視器上,thread-sub子線程運(yùn)行結(jié)束后再喚醒等待在自己監(jiān)視器上的所有線程浦徊。
最后提個問題绍赛,如果是讓主線程等待多個子線程呢?有怎么實(shí)現(xiàn)...
interrupt()
在《Core Java》中有這樣一句話:“沒有任何語言方面的需求要求一個被中斷的程序應(yīng)該終止辑畦。中斷一個線程只是為了引起該線程的注意吗蚌,被中斷線程可以決定如何應(yīng)對中斷 ”。
一個現(xiàn)在未正常結(jié)束之前纯出,被強(qiáng)制終止是很危險(xiǎn)的事情蚯妇,比如終止了一個持有鎖的線程敷燎,那么有可能所有等待鎖的線程都將永久阻塞。Thread中的Thread.suspend線程掛起, Thread.stop線程止等方法都被標(biāo)記Deprecated棄用了箩言。
但有時候我們確實(shí)有必要終止某一個線程硬贯,該怎么做呢,優(yōu)雅的方式便是線程中斷陨收,這里要強(qiáng)調(diào)的是饭豹,線程中斷并非線程終止,而是要給該線程發(fā)一個中斷信號讓它自己決定如何處理务漩,調(diào)用某個線程interrupt()方法拄衰,會設(shè)置該線程的中斷狀態(tài)標(biāo)識來通知它被中斷了。運(yùn)行中的線程只是設(shè)置了中斷狀態(tài)饵骨,isInterrupted()返回true;對于阻塞中的線程翘悉,收到中斷信號后會產(chǎn)生一個中斷異常InterruptedException,同時清除中斷狀態(tài)重新復(fù)位為false居触,打破阻塞狀態(tài)妖混,相當(dāng)于喚醒阻塞(通過wait()、sleep()轮洋、InterruptibleChannel I/O操作制市、Selector阻塞)線程。并非所有阻塞狀態(tài)的線程都能對中斷有響應(yīng)弊予,如中斷信號并不能使BLOCKED (on object monitor)狀態(tài)的死鎖線程恢復(fù)運(yùn)行祥楣。且看源碼:
java源碼比較簡單:
/**
* Interrupts this thread.
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
本地方法c++源碼:
thread.cpp
void Thread::interrupt(Thread* thread) {
trace("interrupt", thread);
debug_only(check_for_dangling_thread_pointer(thread);)
os::interrupt(thread);
}
//windows實(shí)現(xiàn),其他系統(tǒng)應(yīng)該是一樣的邏輯
os_windows.cpp
void os::interrupt(Thread* thread) {
assert(!thread->is_Java_thread() || Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");
OSThread* osthread = thread->osthread();
//設(shè)置中斷標(biāo)識
osthread->set_interrupted(true);
// More than one thread can get here with the same value of osthread,
// resulting in multiple notifications. We do, however, want the store
// to interrupted() to be visible to other threads before we post
// the interrupt event.
OrderAccess::release();
SetEvent(osthread->interrupt_event());
// For JSR166: unpark after setting status
//設(shè)置中斷標(biāo)識后unpark()喚醒線程
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;
}
源碼中有兩行關(guān)鍵的代碼
osthread->set_interrupted(true);
設(shè)置中斷標(biāo)識
if (thread->is_Java_thread()) ((JavaThread*)thread)->parker()->unpark();
//喚醒阻塞線程
java中調(diào)用interrupt()方法后块促,并不能中斷一個正在運(yùn)行的線程荣堰。實(shí)際上是設(shè)置了線程的中斷標(biāo)志位,在線程阻塞的地方(如調(diào)用sleep竭翠、wait振坚、join等地方)拋出一個異常InterruptedException,并且中斷狀態(tài)也將被清除斋扰,重新復(fù)位為false渡八,這樣線程就得以退出阻塞的狀態(tài)。
經(jīng)典代碼示例:
public void run() {
try {
....
while (!Thread.currentThread().isInterrupted() && more work to do){
// do more work;
}
} catch (InterruptedException e) {
// thread was interrupted during sleep or wait
} finally {
// cleanup, if required
}
}
代碼中传货,線程不停的檢查自身的中斷狀態(tài)作為while循環(huán)的條件屎鳍,當(dāng)線程的Thread.interrupt方法被其他線程調(diào)用,中斷狀態(tài)被設(shè)置為true時问裕,退出循環(huán)正常結(jié)束運(yùn)行逮壁。這說明,interrupt中斷只是線程退出邏輯的一部分粮宛,前提是線程需要通過isInterrupted()檢查自己的中斷狀態(tài)窥淆。
中斷阻塞狀態(tài)
對于阻塞狀態(tài)(常見的通過wiat卖宠、sleep等方法進(jìn)行阻塞,這些能拋出InterruptedException異常)的線程忧饭,中斷信號會產(chǎn)生一個中斷異常扛伍,使線程從阻塞狀態(tài)中恢復(fù)運(yùn)行,換句話說就是阻塞被中斷了词裤,線程被喚醒了刺洒。拋出InterruptedException中斷異常后,線程中斷狀態(tài)復(fù)位false吼砂。中斷異常也是線程退出邏輯的一部分逆航。
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author HXJ
* @date 2018/7/26
*/
public class ThreadInterrupt {
public static void main(String[] args) {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//檢查自身中斷狀態(tài)
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep ...");
Thread.sleep(10 * 1000);
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep 結(jié)束");
} catch (InterruptedException e) {
//InterruptedException中斷異常會復(fù)位中斷狀態(tài)為false
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 產(chǎn)生中斷異常!");
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
//檢查自身中斷狀態(tài)
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep ...");
Thread.sleep(10 * 1000);
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep 結(jié)束");
} catch (InterruptedException e) {
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 產(chǎn)生中斷異常帅刊!");
//InterruptedException中斷異常會復(fù)位中斷狀態(tài)為false纸泡,所以需要重新設(shè)置中斷狀態(tài)
Thread.currentThread().interrupt();
}
}
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 正常退出漂问!");
}
});
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " 啟動子線程赖瞒!");
thread.start();
thread1.start();
try {
Thread.sleep(2000);
System.out.println(format.format(new Date()) + " " + Thread.currentThread().getName() + " sleep 2s 結(jié)束,中斷阻塞子線程蚤假!");
thread.interrupt();
thread1.interrupt();
thread.join();
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運(yùn)行結(jié)果:
2018-07-26 14:59:44 main 啟動子線程栏饮!
2018-07-26 14:59:44 Thread-0 sleep ...
2018-07-26 14:59:44 Thread-1 sleep ...
2018-07-26 14:59:46 main sleep 2s 結(jié)束,中斷阻塞子線程磷仰!
2018-07-26 14:59:46 Thread-1 產(chǎn)生中斷異常袍嬉!
2018-07-26 14:59:46 Thread-0 產(chǎn)生中斷異常!
2018-07-26 14:59:46 Thread-1 正常退出灶平!
2018-07-26 14:59:46 Thread-0 sleep ...
2018-07-26 14:59:56 Thread-0 sleep 結(jié)束
2018-07-26 14:59:56 Thread-0 sleep ...
結(jié)果說明伺通,主線程啟動thread、thread1兩個子線程后逢享,開始休眠罐监;兩個子線程啟動后也進(jìn)入長達(dá)10s的休眠狀態(tài);主線程2s后休眠結(jié)束瞒爬,分別中斷了休眠阻塞狀態(tài)的兩個子線程弓柱,兩個子線程產(chǎn)生中斷異常恢復(fù)運(yùn)行侧但,提前結(jié)束休眠狀態(tài)矢空。由于產(chǎn)生中斷異常后中斷狀態(tài)復(fù)位,所以Thread-0子線程的while條件isInterrupted()仍滿足條件繼續(xù)執(zhí)行禀横;而Thread-1在中斷狀態(tài)復(fù)位后interrupt()重新設(shè)置中斷狀態(tài)屁药,while條件不滿足,線程正常退出柏锄。產(chǎn)生中斷異常后酿箭,也可以通過break直接退出循環(huán)體立莉。