最近看多線程的時候發現對於join的理解有些錯誤,在網上查了不少資料,根據自己的理解整理了一下,這里之所以把join和wait放在一起,是因為join的底層實現就是基於wait的,一並講解更容易理解。
wait
了解join就先需要了解wait,wait是線程間通信常用的信號量,作用就是讓線程暫時停止運行,等待其他線程使用notify來喚醒或者達到一定條件自己蘇醒。
wait是一個本地方法,屬於Object類,其底層實現是JVM內部實現,是基於monitor對象監視鎖。
//本地方法
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);
}
//無參數,默認為0
public final void wait() throws InterruptedException {
wait(0);
}
根據源碼可以發現,雖然wait有三個重載的方法,但是主要的還是wait(long timeout)這個本地方法,其他兩個都是基於這個來封裝的,由JVM底層源碼不太好看到,我就以流程的形式來描述。
synchronized (this) {
System.out.println("A begin " + System.currentTimeMillis());
try {
wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A end " + System.currentTimeMillis());
}
- 上述代碼在執行到wait(5000)時首先會釋放當前占用的鎖,並暫停線程。
- 在暫停的5秒內如果收到其他線程的notify()方法發來的信號,那么就再次嘗試獲取已經釋放的鎖
- 如果獲取到那么就繼續執行,沒有就等待鎖釋放來競爭。
- 如果在5秒內未收到信號,那么到時間后就自動蘇醒去嘗試獲取鎖。
而對於時間的參數timeout需要注意的是,如果輸入0不代表不暫停,而是需要特殊情況自己蘇醒或者notify喚醒,這里有個特殊點,wait(0)是可以自己蘇醒的。
public class Thread2 extends Thread{
private Thread1 a;
public Thread2(Thread1 a) {
this.a = a;
}
@Override
public void run() {
synchronized (a) {
System.out.println("B begin " + System.currentTimeMillis());
try {
a.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B end " + System.currentTimeMillis());
}
}
}
public class Thread1 extends Thread{
@Override
public void run() {
synchronized (this) {
System.out.println("A begin " + System.currentTimeMillis());
System.out.println("A end " + System.currentTimeMillis());
}
}
}
public class Main{
public static void main(String[] args) {
Thread1 thread = new Thread1();
Thread2 thread2 = new Thread2(thread);
thread2.start();
thread.start();
System.out.println("main end "+System.currentTimeMillis());
}
}
這個例子運行結果存在以下情況
B begin 1494995803564
main end 1494995803565
A begin 1494995803565
A end 1494995803565
B end 1494995803565
wait()在沒有notify()情況下自動蘇醒了,因此這里可以看到,當前情況下Thread.wait()等待過程中,如果Thread結束了,是可以自動喚醒的。這個會在join中被使用。
join
了解了wait的實現原理之后就可以來看join了,join是Thread類的方法,不是底層本地方法,這里可以看一下它的源碼。
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
//參數判斷<0,拋異常
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//參數為0,即join()
if (millis == 0) {
//當前線程存活,就調用wait(0),一直到調用join的線程結束再自動蘇醒
while (isAlive()) {
wait(0);
}
//參數>0,調用wait(long millis)等待一段時間后自動喚醒
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
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);
}
public final void join() throws InterruptedException {
join(0);
}
很明顯,join的三個重載方法主要還是基於join(long millis)方法,因此我們主要關注這個方法,方法的處理邏輯如下
- 判斷參數時間參數,如果參數小於0,拋出IllegalArgumentException("timeout value is negative")異常
- 參數等於0,判斷調用join的線程(假設是A)是否存活,不存活就不執行操作,如果存活,就調用wait(0),阻塞join方法,等待A線程執行完在結束join方法。
- 參數大於0,判斷調用join的A線程是否存活,不存活就不執行操作,如果存活,就調用wait(long millis),阻塞join方法,等待時間結束再繼續執行join方法。
由於join是synchronized修飾的同步方法,因此會出現join(long millis)阻塞時間超過了millis的值。
public class Thread1 extends Thread{
@Override
public void run() {
synchronized (this) {
System.out.println("A begin " + System.currentTimeMillis());
try {
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A end " + System.currentTimeMillis());
}
}
}
public class Main{
public static void main(String[] args) {
Thread1 thread = new Thread1();
thread.start();
try {
thread.join(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("main end "+System.currentTimeMillis());
}
}
這個例子的運行結果是
A begin 1494996862054
A end 1494996867056
main end 1494996867056
main線程一定是最后執行完的,按照流程來說,1秒之后阻塞就結束了,main線程應該就可以開始執行了,但是這里有一個注意點,join(long millis)在執行millis>0的時候在wait(delay)之后還有一行代碼,而上面代碼1秒之后只是結束了wait方法,並沒有執行完join方法。上面的例子,由於join的鎖和thread的鎖相同,在thread運行完之前,鎖不會釋放,那么導致join一直阻塞在最后一步無法結束,才會出現上面的情況。