再深一点,理解线程的join方法
配图:曲径通幽
讲真,如果不是被面试官吊打,join()方法也还不会引起我的重视。因为,工作中确实没有使用过它。
现在,对它来个刨根问底。
join()方法的作用
在写这篇文章之前,我对join的理解只停留在字面意思“把指定线程加入到当前线程”。
再来看官方怎么解释的:
//Waits for this thread to die.
public final void join() throws InterruptedException {
join(0);
}
“Waits for this thread to die.”,也就是等着join()方法所属的 线程死亡(run方法执行完毕正常结束或线程异常死亡)。
下面举个例子,证实这个说法。
举个栗子
import static java.lang.System.out;
public class JoinTest {
public static void main(String args[]) throws InterruptedException{
String threadName = Thread.currentThread().getName();
out.println(threadName + " is Started");
Thread th1 = new FooThread();
th1.start();
th1.join();
out.println(threadName + " is Completed");
}
}
public class FooThread extends Thread{
public void run(){
try {
String threadName = Thread.currentThread().getName();
out.println(threadName + " is Started");
Thread.sleep(2000);
out.println(threadName + " is Completed");
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
输出结果:
main is Started
Thread-0 is Started
Thread-0 is Completed
main is Completed
例子中,首先主线程main thread开始执行,接着主线程创建并启动了另外一个线程“Thread-0”。因为Thread-0睡了2秒,所以线程Thread-0至少需要2秒才能执行完成。一般情景,主线程启动线程Thread-0后,会继续自己的工作,而不关心线程Thread-0的执行情况。但是由于join()方法的调用,主线程必须等待,直到Thread-0执行完成,主线程才可以继续执行后面的代码。这个执行顺序通过输出结果也可以看出。
也可以说线程Thread-0加入了正在执行的主线程,这样理解更贴切方法名join。
继续看源代码{join(0)}方法的实现。
实现原理
/*
* Waits at most millis milliseconds for this thread to die.
* A timeout of 0 means to wait forever.
*/
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) {//join(0)
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
注释的意思是,等待这个线程最多millis毫秒,millis毫秒后,不管这个线程有没有死亡,主线程继续执行。
超时时间为0,意味着主线程要永远等待。
进入{if(millis === 0)}条件模块后,首先根据isAlive()判断这个线程(例子中的Thread-0线程)是否还活着,如果活着,阻塞主线程(例子中的main线程)。把{wait(0)}放在while循环中,是为了防止主线程阻塞期间被其他线程唤醒。
也就说,此时主线程main会被一直阻塞,直到Thread-0线程执行结束。
But !
Thread-0执行结束后,即whie(isAlive)返回false时,join方法随之也就结束了。并没有看到唤醒主线程的代码?
其实join方法的注释中还有这样一句话:
“As a thread terminates the this.notifyAll method is invoked.
It is recommended that applications not use wait, notify, or notifyAll on Thread instances.”
当一个线程结束的时候,会主动调用{this.notifyAll}唤醒所有等待该线程对象锁(例子中的th1)的所有线程。并且,不建议在应用程序中调用该线程对象的wait,notify,notifyAll方法。
看完这句话,前一秒豁然开朗;后一秒,我还是想问“notifyAll在哪调用的,还是没看到?”
带着这个问题我google了一下,找到了答案。答案说这段代码在jvm code中,并没有贴出具体代码。
自己找呗。
JVM源码
我并没有下载HotSpot的代码,因为庞大且复杂,想找到目标代码不容易。而是下载了超小型Java虚拟机JamVM。有多小?HotSpot源代码一百多兆,JamVM只有656kb,你感受下?(表情:苦笑、苦笑)
麻雀虽小五脏俱全,JamVM的目标是支持最新版的Java虚拟机规范。研究JVM原理,它是个不错的入门选择。
thread.c#threadStart(void *arg)
threadStart负责初始化并执行线程的run方法
void *threadStart(void *arg){
Thread *thread = (Thread *)arg;
Object *jThread = thread->ee->thread;
enableSuspend(thread);
//初始化线程结构体,创建线程栈等
initThread(thread, INST_DATA(jThread, int, daemon_offset), &thread);
/* Add thread to thread ID map hash table. */
addThreadToHash(thread);
/* 执行线程的run方法 */
executeMethod(jThread, CLASS_CB(jThread->class)->method_table[run_mtbl_idx]);
/* run方法执行完毕。分离线程并退出 */
detachThread(thread);
TRACE("Thread 0x%x id: %d exited\n", thread, thread->id);
return NULL;
}
可以看到,detachThread方法负责善后线程执行结束后的工作。
thread.c#detachThread(Thread *thread)
void detachThread(Thread *thread) {
//省略...
/* Remove thread from the ID map hash table */
deleteThreadFromHash(thread);
/* 唤醒所有等待VMThread对象的线程 */
objectLock(vmthread);
objectNotifyAll(vmthread);
objectUnlock(vmthread);
/* Disable suspend to protect lock operation */
disableSuspend(thread);
/* 从Thread链表中删除 */
if((thread->prev->next = thread->next))
thread->next->prev = thread->prev;
/* 线程数减一 */
threads_count--;
/* 回收线程ID */
freeThreadID(thread->id);
/* 释放系统资源*/
sysFree(ee->stack);
sysFree(ee);
//省略...
}
在善后工作中,首先要做的就是唤醒所有在等待该线程对象的线程,然后是回收系统资源等。
再谈join的应用
join方法可以实现让多线程按指定顺序执行,这点在需要多线程相互协作工作的业务场景中很重要。
需求:计算1+2+3+...+100的结果。
为了提高计算速度,我们启动两个线程并行计算,线程leftThread计算1到50的和,线程rightThread计算51到100的和。
线程sumThread负责合并最后的计算结果,所以线程sunThread必须等待leftThread和rightThread执行结束后,才能计算最后的结果,这里就需要把两个计算线程join到sumThread线程中。sumThread的run方法如下:
public void run(){
int leftResult = leftThread.join();
int rightResult = rightThread.join();
sum = leftResult + right Result;
}
But !
有个问题,join方法是不能返回线程的计算结果的。怎么办?
幸运的是,JDK中为我们提供了现成的解决方案。在jdk7中,concurrent包的作者Doug Lea给我们带来了一个高效的并行计算框架Fork/Join。Fork/Join模式极大的简化了开发并发程序的繁琐工作。But! 这个框架不是这篇文章的重点,之所以提到它是因为这是join方法的一个很有力的应用案例。感兴趣自己研究一下吧。