深入理解Java併發編程之通過JDK C++源碼以及Debug源碼死扣Thread.join()

本文轉載自個人掘金博客:https://juejin.im/post/5ece5f71f265da76de5cda58

基本含義

如果一個線程A執行了thread.join()語句,其含義是:當前線程A等待thread線程終止之後才從thread.join()返回。

線程Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示,如果線程thread在給定的超時時間裏沒有終止,那麼將會從該超時方法中返回。

實現原理

首先介紹下線程的狀態

線程的狀態

Java線程在運行的生命週期中可能處於6種不同的狀態,在給定的一個時刻,線程只能處於其中的一個狀態。如下內容截取JDK 1.8 Thread.java的源碼:

  1. NEW: 初始轉態,線程被構建,但是還沒有調用start()方法。
  2. RUNNABLE: 正在執行的線程狀態,JVM中runnable線程狀態對應於操作系統中的就緒和運行兩種狀態。
  3. BLOCKED: 線程等待monitor互斥量的阻塞狀態,在blocked狀態的線程正在被執行Object.wait()後等着進入或者再次同步塊或者同步方法。
  4. WAITING: 等待狀態,下列方法會導致線程處於等待狀態:
    • Object.wait with no timeout
    • Thread.join with on timeout
    • LockSupport.park
  5. TIMED_WAITING: 超時等待,超過等待時間便會自動返回運行狀態,下列方法會導致線程處於超時等待狀態:
    • Thread.sleep
    • Object.wait(long) with timeout
    • Thread.join(long) with timeout
    • LockSupport.parkNanos
    • LockSupport.parkUntil
  6. TERMINATED: 線程完成執行後結束的狀態。

在介紹下Monitor

Monitor

Monitor是 Java中用以實現線程之間的互斥與協作的主要手段,它可以看成是對象的鎖。每一個對象都有,也僅有一個 monitor。

在HotSpot JVM中,monitor是由ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中主要有以下4個參數:

  1. _Owner: 用於指向ObjectMonito對象的線程
  2. _EntrySet:用來保存處於blocked狀態的線程列表
  3. _WaitSet: 用來保存處於waiting狀態的線程
  4. _count: 計數器

當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置爲當前線程。同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 _WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。如下圖所示:

 

 

 

實現機制

一個簡單的例子。

public class ThreadA {
    public static void main(String[] args) {
        Runnable r = () -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("子線程執行完畢");
        };
        Thread threadB = new Thread(r, "Son-Thread");
        //啓動線程
        threadB.start();
        try {
            //調用join()方法
            threadB.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主線程執行完畢");
        System.out.println("~~~~~~~~~~~~~~~");
    }
}

底層是如何實現join()語義的呢,以上面的例子舉例。

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    ...
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ...
        Thread parent = currentThread();                     
        ...
            if (g == null) {
                g = parent.getThreadGroup();
            }
       ...
   }
   ...
    public synchronized void start() {
    	...
        group.add(this);
        ...
    }
  1. 由於join(long millis)方法加了對象鎖,鎖的是Thread類當前對象實例即threadB。同時,Thread.start()方法在啓動後,threadB也持有自己線程對象實例的所有內容,包括對象實例threadB的對象鎖。具體可參見start0()源碼
    public final void join() throws InterruptedException {
        join(0);
    }
	...
    public final synchronized void join(long millis)
    throws InterruptedException {
	...
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        }
     ...
    }
  1. 如果threadB線程在join()方法前執行完了,釋放了對象鎖,threadA獲取鎖進入同步方法join(long millis)時,調用threadB的方法isAlive()判斷threadB線程已經不存活,那麼執行完join()邏輯退出,繼續執行threadA的邏輯。

Object.java

    /**
     * The current thread must own this object's monitor. Causes the current thread to wait until either another thread invokes the method...
     * This method causes the current thread call it to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object.
     */
    public final native void wait(long timeout) throws InterruptedException;
  1. 如果threadB線程在join()方法前沒執行完,並且由於某種原因釋放了對象鎖,當threadA獲取鎖進入同步方法join(long millis)時,調用threadB的方法isAlive()判斷threadB線程還存活。於是,threadA就調用native方法wait()釋放鎖並進行等待(threadA進入threadB對應的monitor對象的Wait Set,此時threadA的線程狀態爲waiting)。以便這個對象鎖能被threadB獲取繼續執行。直到threadB執行完成,釋放鎖並結束。
    /**
     * This method is called by the system to give a Thread
     * a chance to clean up before it actually exits.
     */
    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

ThreadGroup.java

    void threadTerminated(Thread t) {
        synchronized (this) {
            remove(t);
            if (nthreads == 0) {
                notifyAll();
            }
			...
        }
    }
  1. threadB線程結束時會執行exit()方法,進行一些資源的清理。從源碼的註釋可以發現,這個時候實際上線程是事實上存在的。那麼是誰喚醒waiting的threadA呢?

錯誤解釋:有很多博文的大致解釋如下:threadB線程結束時會執行exit()方法,notifyAll()同一線程組的其他線程。threadA線程在new threadB的時候,threadA和threadB共享一個線程組。同時線程初始化的時候,線程所在的線程組都包含線程本身,於是threadB的線程組會包含threadA。那麼,threadB結束時threadA會被notify。

這個解釋是錯誤的,爲什麼呢?由於if (nthreads == 0)的觸發條件不滿足,threadA和threadB共享一個線程組,當threadB被移除了,threadA還在線程組中,nthreads = 1。

/jdk7/hotspot/src/os/linux/vm/os_linux.cpp

int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);

static void *java_start(Thread *thread) {
  ...
  thread->run();
  return 0;
}

/jdk7/hotspot/src/share/vm/runtime/thread.cpp

void JavaThread::run() {
  ...
  thread_main_inner();
}

void JavaThread::thread_main_inner() {
  ...
  this->exit(false);
  delete this;
}

void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
  ...
  // Notify waiters on thread object. This has to be done after exit() is called
  // on the thread (if the thread is the last thread in a daemon ThreadGroup the
  // group should have the destroyed bit set before waiters are notified).
  ensure_join(this);
  ...
}

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();
}

正確解釋:在線程native代碼的run()方法的結束,native代碼會將線程的alive狀態置爲false,同時會notifyAll等待在這個線程實例上的所有其他線程。根據上面的c++源碼,是lock.notify_all(thread) 這個動作會notify所有等待在當前線程實例上的其他線程。

  1. 那麼,threadB結束時threadA會被notify,從而threadB對應的monitor對象的Wait Set移動到該monitor對象的Entry Set,線程狀態變爲Blocked,等待調度獲取monitor的控制權。

  2. threadA獲取monitor的控制權後,繼續執行while (isAlive()) 循環,此時isAlive()爲false。那麼執行完join()邏輯退出,繼續執行threadA的邏輯。

通過綜上的設計,Thread.join()實現了當前線程A等待thread線程終止之後才從thread.join()返回的設計邏輯。

Debug分析

我們通過上面的那個簡單的例子來Debug逐點分析:

  1. 當主線程執行到join()邏輯中時,是RUNNING的狀態

 

 

 

 

 

 

  1. 當子線程執行到exit()邏輯時,threadB依舊是存活,狀態爲RUNNING

 

 

 

 

 

 

threadA的狀態爲WAIT

 

 

 

 

 

 

  1. threadB執行到threadTerminated()邏輯,這時候發現nthreads:1,根本不會執行notifyAll()操作。就算執行了notifyAll()操作,鎖的對象都不一樣。一個是threadB的實例,一個是線程組的實例。

 

 

 

等待/通知的經典範式

可以發現Thread.join()方法與等待/通知的經典範式中的等待範式如出一轍。 而Thread.exit()方法則有點類似於其中的通知範式。

等待/通知的經典範式分爲兩個部分:等待方和通知方。 等待方遵循如下原則:

  1. 獲取對象的鎖。
  2. 如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
  3. 條件滿足則執行對應的邏輯。 對應的僞代碼如下:
synchronized(對象) {
	while(條件不滿足) {
		對象.wait();
	}
	對應的處理邏輯
}

通知方遵循如下原則:

  1. 獲得對象的鎖。
  2. 改變條件。
  3. 通知所有等待在對象上的線程。 對應的僞代碼如下:
synchronized(對象) {
	改變條件
	對象.notifyAll();
}

最後,覺得寫的不錯的同學麻煩點個贊,支持一下唄^_^~

參考與感謝

  1. 《Java併發編程的藝術》
  2. https://stackoverflow.com/questions/9866193/who-and-when-notify-the-thread-wait-when-thread-join-is-called
  3. https://www.jianshu.com/p/81a56497e073
  4. https://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html
  1.  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章