Thread join的原理和使用剖析
官方解释thread join
很多人对Thread.join的作用以及实现了解得很少,毕竟这个api我们很少使用。这篇文章仍然会结合使用及原理进行深度分析。
void join() 等待这个线程死亡。
void join(long millis) 等待这个线程死亡最多 millis毫秒。
我们可以看到这样的解释还是有点不准确,大白话其实就等待一个线程从一个RUNNABLE状态到线程运行结束。
Thread 面试
Java中如何让多个线程按照自己指定的顺序执行?
这个问题最简单的回答是通过Thread.join来实现,但是这样就有会有一个问题,时间久了就让很多人误以为Thread.join是用来保证线程的顺序性的。下面这段代码演示了Thread.join的作用
本工程是用maven构建
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.changhong.threadtest</groupId>
<artifactId>thread-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
</dependencies>
</project>
这个例子就是等待 join-first-thread 2秒后线程结束了,在执行后面的join-second-thread 线程。
package com.changhong.thread.chapter1;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadJoinTest {
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(1);
final Thread first=new Thread(new Runnable() {
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
log.error("current thread run focus something wrong , messages is [{}]",e.toString());
}
log.error("this is a job-num-0");
}
},"join-first-thread");
first.start();
final Thread second=new Thread(new Runnable() {
public void run() {
try {
first.join();
} catch (InterruptedException e) {
log.error("current thread run focus something wrong , messages is [{}]",e.toString());
}
log.error("this is a job-num-1");
}
},"join-second-thread");
second.start();
} catch (InterruptedException e) {
log.error("the program have error focus [{}]",e.toString());
}
}
}
控制台打印的结果如下:
2019-09-13 18:40:52.714 [join-first-thread] ERROR com.changhong.thread.chapter1.ThreadJoinTest - this is a job-num-0
2019-09-13 18:40:52.716 [join-second-thread] ERROR com.changhong.thread.chapter1.ThreadJoinTest - this is a job-num-1
Thread.join的实现原理
线程是如何被阻塞的?又是通过如何唤醒的呢?先来看看JDK Thread.join的源码是如何实现的?
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
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;
}
}
}
从jdk join方法的源码来看,join方法的本质调用的是Object中的wait方法实现线程的阻塞。但是我们知道,调用wait方法必须要获取锁,所以join方法是被synchronized修饰的,synchronized修饰在方法层面相当于synchronized(this),this就是join-first-thread本身的实例。
有很多人不理解join为什么阻塞的是join-first-thread线程呢? 不理解的原因是阻塞join-second-thread线程的方法是放在join-first-thread这个实例作用,让大家误以为应该阻塞join-first-thread线程。实际上join-second-thread线程会持有join-first-thread这个对象的锁,然后调用wait方法去阻塞,而这个方法的调用者是在join-second-thread线程中的。所以造成主线程阻塞。
第二个问题,为什么join-first-thread线程执行完毕就能够唤醒第二个线程呢?或者说是在什么时候唤醒的?
我们打开 Thread类的源码如下:
public
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
private volatile String name;
private int priority;
private Thread threadQ;
private long eetop;
/* Whether or not to single_step this thread. */
private boolean single_step;
.......省略代码
我们可以看到这里有一个本地注册方法的代码 registerNatives();
这个方法放在一个static语句块中,当该类被加载到JVM中的时候,它就会被调用,进而注册相应的本地方法。而本地方法registerNatives()是定义在Thread.c文件中的。
static JNINativeMethod methods[] = {
{"start0", "()V",(void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive","()Z",(void *)&JVM_IsThreadAlive},
{"suspend0","()V",(void *)&JVM_SuspendThread},
{"resume0","()V",(void *)&JVM_ResumeThread},
{"setPriority0","(I)V",(void *)&JVM_SetThreadPriority},
{"yield", "()V",(void *)&JVM_Yield},
{"sleep","(J)V",(void *)&JVM_Sleep},
{"currentThread","()" THD,(void *)&JVM_CurrentThread},
{"countStackFrames","()I",(void *)&JVM_CountStackFrames},
{"interrupt0","()V",(void *)&JVM_Interrupt},
{"isInterrupted","(Z)Z",(void *)&JVM_IsInterrupted},
{"holdsLock","(" OBJ ")Z",(void *)&JVM_HoldsLock},
{"getThreads","()[" THD,(void *)&JVM_GetAllThreads},
{"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads}
};
如果想要彻底的分析这个这个问题,我们必须找到jdk的源码,但是如果大家对线程有一定的基本了解的话,通过wait方法阻塞的线程,需要通过notify或者notifyall来唤醒。所以在线程执行完毕以后会有一个唤醒的操作,只是我们不需要关心。接下来在hotspot的源码中找到 thread.cpp,看看线程退出以后有没有做相关的事情来证明我们的猜想。
if (millis == 0) {
//直到该线程死亡才结束
while (isAlive()) {
wait(0);
}
}
我们看到thread.cpp的代码有这样的逻辑。
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
//如果发现线程结束,就return fasle
{ 这个是java代码里面的逻辑
if (millis == 0) {
//直到该线程死亡才结束
while (isAlive()) {
wait(0);
}
}
}
java_lang_Thread::set_thread(threadObj(), NULL);
lock.notify_all(thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}
从这个源码可以看到ensure_join方法中,调用 lock.notify_all(thread); 唤醒所有等待thread锁的线程,意味着调用了join方法被阻塞的线程会被唤醒,到目前为止,我们基本上对join的原理做了一个比较详细的分析。
总结
1,Thread.join其实底层是通过wait=notifyall来实现线程通信达到线程阻塞
2,当线程执行结束以后,java_lang_Thread::set_thread(threadObj(), NULL); 调用这个设置native线程对象为null,lock.notify_all(thread);让等待在对象锁上的wait方法被唤醒。
该博客为独秀天狼原创,转载请注明出处。