本來是想把標題定為《Thread的wait與join》焊虏,后來想想不嚴謹沮翔,因為wait是Object的方法焰望,不是Thread獨有的骚亿,所以這里要注意一下。
關于wait()方法熊赖,在Object中有三個重載方法:
public final native void wait(long timeout) throws InterruptedException;
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);
}
public final void wait() throws InterruptedException {
wait(0);
}
無論是哪個来屠,最終都是要調用本地方法 wait(long timeout) 。
開局一張圖:
今天主要就來講一下wait()震鹉,join()的關系吧俱笛。
wait
一個Object的方法,目的是將調用obj.wait()的線程置為waiting的狀態(tài)传趾,等待其他線程調用obj.notify()或者obj.notifyAll()來喚醒迎膜。最常見的就是生產者/消費者功能。
有一點注意的就是浆兰,wait/notify方法的調用必須處在該對象的鎖(Monitor)中磕仅,也即,在調用這些方法時首先需要獲得該對象的鎖簸呈。否則會拋出IllegalMonitorStateException異常榕订。
wait/notify的通俗解釋:
1.線程A首先獲取到obj的鎖,然后執(zhí)行了obj.wait()蜕便,這個方法就會是線程A暫時讓出對obj鎖的持有劫恒,并把線程A轉換waiting狀態(tài),同時加入鎖對象的等待隊列轿腺。
2.線程B獲取到了obj的鎖两嘴,然后執(zhí)行了obj.notify()丛楚,這個方法通知了鎖對象的等待隊列,使正在等待隊列中的線程A改為阻塞狀態(tài)憔辫,使A進入對obj鎖的競爭鸯檬。當然在執(zhí)行notify后并不會使線程A馬上獲取到鎖,因為線程B目前還在持有obj的鎖螺垢。
3.線程A獲取到obj鎖,繼續(xù)從wait()之后的代碼運行赖歌。
舉個例子:
public class ThreadTest {
static final Object obj = new Object(); //對象鎖
private static boolean flag = false;
public static void main(String[] args) throws Exception {
Thread consume = new Thread(new Consume(), "Consume");
Thread produce = new Thread(new Produce(), "Produce");
consume.start();
Thread.sleep(1000);
produce.start();
try {
produce.join();
consume.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生產者線程
static class Produce implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("進入生產者線程");
System.out.println("生產");
try {
TimeUnit.MILLISECONDS.sleep(2000); //模擬生產過程
flag = true;
obj.notify(); //通知消費者
TimeUnit.MILLISECONDS.sleep(1000); //模擬其他耗時操作
System.out.println("退出生產者線程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消費者線程
static class Consume implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("進入消費者線程");
System.out.println("wait flag 1:" + flag);
while (!flag) { //判斷條件是否滿足枉圃,若不滿足則等待
try {
System.out.println("還沒生產,進入等待");
obj.wait();
System.out.println("結束等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait flag 2:" + flag);
System.out.println("消費");
System.out.println("退出消費者線程");
}
}
}
}
結果:
進入消費者線程
wait flag 1:false
還沒生產庐冯,進入等待
進入生產者線程
生產 // 1秒后notify
退出生產者線程 //生產線程結束
結束等待
wait flag 2:true
消費
退出消費者線程
可以看到孽亲,生產線程在notify后,消費線程并沒有馬上繼續(xù)運行展父,原因就是上面提到的第二點返劲。
wait()方法有了一個大概的了解,下面看看join的原理栖茉。
join
join方法通常的解釋就是等待調用線程執(zhí)行完畢后篮绿,再繼續(xù)執(zhí)行當前線程。通常用在多線程的業(yè)務上吕漂,某個線程的運算需要另一個線程的結果時亲配,就可以使用join。
來個例子:
public class JoinMainDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread 1");
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread 2");
}
});
Thread thread3 = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread 3");
}
});
thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
thread3.join();
System.out.println("thread Main");
}
}
情況1輸出:
thread 1
thread 2
thread 3
thread Main
流程:Main線程調用thread1join后惶凝,會等待join執(zhí)行完畢才會繼續(xù)運行吼虎,thread2,thread3都是這樣苍鲜。1思灰,2,3順序是固定的混滔。
我們改一下代碼順序
thread1.start();
thread3.start();
thread2.start();
thread1.join();
thread2.join();
thread3.join();
情況2輸出:
thread 3
thread 1
thread 2
thread Main
結果是線程1洒疚,2,3隨機順序遍坟,Main一定在最后拳亿。
去掉join:
thread1.start();
thread3.start();
thread2.start();
情況3輸出:
thread Main
thread 3
thread 1
thread 2
這個就是普通的線程執(zhí)行結果。
我們來分析一下每種情況的原因愿伴。join的行為像不像被wait后自動釋放的過程肺魁?看一下join的實現:
public final void join() throws InterruptedException {
join(0);
}
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) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
可見,線程A在調用obj.join方法時隔节,obj線程如果是alive狀態(tài)(線程開啟start鹅经,但未結束)寂呛,那么就執(zhí)行wait方法。同時看join(long m)方法是synchronized修飾的瘾晃,這是我們使用wait時需要先獲取鎖的前置條件贷痪。既然知道了join的內核是wait方法,通過對wait的了解蹦误,線程A此時是waiting的狀態(tài)劫拢,并進入了obj鎖的等待隊列排隊去。那么是誰在什么時候釋放了線程A呢强胰?
這時要了解一下Thread.exit()方法:
/**
* 這個方法由系統調用舱沧,當該線程完全退出前給它一個機會去釋放空間。
*/
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
這個方法由系統調用偶洋,當該線程完全退出前給它一個機會去釋放空間熟吏。再往下跟到threadTerminated(this)方法
void threadTerminated(Thread t) {
synchronized (this) {
remove(t);
if (nthreads == 0) {
notifyAll();
}
if (daemon && (nthreads == 0) &&
(nUnstartedThreads == 0) && (ngroups == 0))
{
destroy();
}
}
}
這里有一個notifyAll()的方法,可見就是在這里是線程A得到了釋放繼續(xù)運行玄窝。
到這里就解釋通了牵寺,join的確是通過wait方法使調用線程變?yōu)榈却隣顟B(tài),再在被調用線程運行結束時通過系統調用exit方法啟動了notifyAll恩脂。
我們可以這么理解帽氓,把obj.join方法替換成obj.wait()方法,并且在obj線程運行結束后自動執(zhí)行notifyAll()方法东亦,這樣就可以用wait的思路來理解join的運行過程了杏节。
好了,現在回到上面例子的三種情況:
情況1: 可以發(fā)現典阵,thread2.start前有thread1.join奋渔,thread3.start前有thread2.join,這樣就能解釋thread1,2,3是按順序執(zhí)行的壮啊。thread1.join的時候嫉鲸,Main線程進入了thread1鎖對象的等待隊列,只有thread1運行完成后才會得到釋放歹啼。進而才會按順序開啟線程玄渗,thread2.start,thread3.start狸眼。
情況2:先將3個線程開啟藤树,再依次執(zhí)行join。join之前拓萌,三個線程已經都在運行岁钓,所以輸出的順序并沒有固定,只是會控制Main線程運行時間。Main的輸出肯定是在最后的屡限。當然我們可以把thread2的sleep時間調大一點品嚣,并且不執(zhí)行join再來運行看看結果,自己嘗試解釋一下钧大。
情況3:沒有join翰撑,各個線程獨自運行,互不影響啊央。
總結
1.wait的注意點: wait方法是Object的方法眶诈; wait/notify方法需要獲得對象鎖后執(zhí)行。
2.wait方法會把調用線程轉為等待waiting狀態(tài)瓜饥,釋放對象鎖册养,并進入對象鎖的等待隊列。
3.notify/notifyAll方法會喚醒對象鎖的等待隊列压固,使其中的線程進入阻塞blocking狀態(tài)搶占對象鎖。
4.調用notify/notifyAll后靠闭,在notify/notifyAll的前獲得的對象鎖得到釋放后帐我,等待隊列里的線程才有機會搶占鎖繼續(xù)執(zhí)行。
5.join方法的內核就是wait愧膀。在被調用對象的線程運行完畢后拦键,系統自動調 用被調用對象notifyAll方法。