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方法被喚醒。
該博客爲獨秀天狼原創,轉載請註明出處。