Thread的Join方法原理
本文轉載自微信公眾號「編了個程」,作者Yasin x 。轉載本文請聯系編了個程公眾號。
Y說
今天沒什么要說的。我個人很喜歡拍天空的照片,放一張前段時間晚上拍的照片吧。
join方法釋放鎖嗎?
前段時間,有一個讀者私信我,問了這么一個問題:Thread實例的join方法內部是調用的wait方法,而wait方法是會釋放鎖的,為什么網上很多文章(包括我們之前寫的開源書《深入淺出Java多線程》)會說join方法不釋放鎖?
釋放thread對象鎖
我們先用書中的一個例子說起:
- public class Join {
- static class ThreadA implements Runnable {
- @Override
- public void run() {
- try {
- System.out.println("我是子線程,我先睡一秒");
- Thread.sleep(1000);
- System.out.println("我是子線程,我睡完了一秒");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- public static void main(String[] args) throws InterruptedException {
- Thread thread = new Thread(new ThreadA());
- thread.start();
- thread.join();
- System.out.println("如果不加join方法,我會先被打出來,加了就不一樣了");
- }
- }
在這個例子中,我們在main方法中調用了thread.join(),打印出來的效果就是:
- 我是子線程,我先睡一秒
- 我是子線程,我睡完了一秒
- 如果不加join方法,我會先被打出來,加了就不一樣了
這個例子想要表達的意圖很簡單,就是通過thread實例的join方法,達到main線程等待thread線程執行完后再繼續執行的效果。
那join方法底層是如何實現這個功能的呢?究竟會不會釋放鎖呢?我們點進去看看源碼。
- if (millis == 0) {
- while (isAlive()) {
- wait(0);
- }
- } else {
- while (isAlive()) {
- long delay = millis - now;
- if (delay <= 0) {
- break;
- }
- wait(delay);
- now = System.currentTimeMillis() - base;
- }
- }
可以看到,join的底層是調用的wait(long)方法。而wait方法是Object類型的實例方法,會釋放當前Object的鎖,且需要拿到當前Object的鎖才行。
這么說可能有點繞。眾所周知,「Java的鎖其實本質上是對象鎖」,因為我們前面調用的是thread.join(),所以這里的“鎖”對象其實thread這個對象。那這里wait釋放的是thread這個對象鎖。
我們把上面的main方法簡單改一下,用另一個線程是占住thread這個對象鎖,就比較直觀了:
- public static void main(String[] args) throws InterruptedException {
- Thread thread = new Thread(new ThreadA());
- thread.start();
- new Thread(() -> {
- // 把thread對象作為鎖占住,這樣下面的join里面的wait只有等鎖釋放了才能執行。
- synchronized (thread) {
- try {
- System.out.println("我占住了thread鎖");
- Thread.sleep(10000);
- System.out.println("我thread鎖釋放了");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }).start();
- thread.join();
- System.out.println("如果不加join方法,我會先被打出來,加了就不一樣了");
- }
打印結果:
- 我是子線程,我先睡一秒
- 我占住了thread鎖
- 我是子線程,我睡完了一秒
- 我thread鎖釋放了
- 如果不加join方法,我會先被打出來,加了就不一樣了
這就印證了那句話:wait方法執行前,是需要獲取當前對象的鎖的。
所以回歸到最開始的問題:join()方法會釋放鎖嗎?嚴瑾的答案是它會釋放thread實例的對象鎖,但不會釋放其它對象鎖(包括main線程)。stackoverflow也對這個有討論:Does Thread.join() release the lock? Or continue to hold it?。
簡單來說,你說它釋放了鎖也對,因為它確實通過wait方法釋放了thread對象鎖,你說它沒釋放鎖也對,因為從調用線程的角度來看,它并沒有釋放當前調用線程持有的對象鎖。
當然,為了防止其它讀者看到這也有這個疑惑,我直接把文中的這句話刪掉了。
^image.png^
誰喚醒了?
源碼看到這,我又有了一個新的疑問:join方法內部是一個while循環。wait釋放了鎖,那必然會有一個人來喚醒它,程序才能夠繼續往下走。那必然有一個地方調用了thread對象的notify方法。
我們在Thread類里面可以找到一個exit()方法,上面備注寫著:This method is called by the system to give a Thread a chance to clean up before it actually exits.
這么簡單的英文大家應該都能看懂吧?
里面有這么一段代碼:
- if (group != null) {
- group.threadTerminated(this);
- group = null;
- }
- void threadTerminated(Thread t) {
- synchronized (this) {
- remove(t);
- if (nthreads == 0) {
- notifyAll();
- }
- if (daemon && (nthreads == 0) &&
- (nUnstartedThreads == 0) && (ngroups == 0))
- {
- destroy();
- }
- }
- }
一開始我以為是在這里喚醒的,但仔細一看,這里調用的對象是ThreadGroup的實例,而不是thread實例。所以應該不是這個地方。
經過一通google之后,我又在stackoverflow上找到了正確的答案(stackoverflow, yyds):who and when notify the thread.wait() when thread.join() is called?
答案顯示,這是在JVM層面去做的事:
- 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
- java_lang_Thread::set_thread(threadObj(), NULL);
- lock.notify_all(thread);
- // Ignore pending exception (ThreadDeath), since we are exiting anyway
- thread->clear_pending_exception();
- }
可以看到除了notify_all以外,它其實做了很多掃尾的工作。包括處理異常、設置線程狀態等。
如果線程沒啟動
再把代碼改一下,如果線程沒有通過start啟動會怎樣呢?
- Thread thread = new Thread(new ThreadA());
- // thread.start();
- thread.join();
- System.out.println("如果不加join方法,我會先被打出來,加了就不一樣了");
會直接執行最后一行代碼打印出來。
看join源碼就知道了,在wait之前,會有一個isAlive()的判斷,看當前線程是否是alive的。如果沒有start,那就會直接返回false,不進入wait。
總結
join方法會釋放thread對象鎖,底層是wait方法,在JVM層面通過notify_all來喚醒的。