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

基本含義

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

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

實現原理

首先介紹下線程的狀態linux

線程的狀態

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

  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。bash

在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(鎖)。以下圖所示:ide

實現機制

一個簡單的例子。ui

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的對應的monitor。具體可參見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全部等待在當前線程實例上的其餘線程。

除了看C++的源碼驗證,咱們也寫了一個demo來驗證這點,waitThread執行完結束後後,wait()在waitThread對象實例的其餘線程纔會被喚醒繼續執行。

/**
     * Wait Thread wait thread.
     * Run Thread1 run thread outer.
     * Run Thread2 run thread outer.
     * Run Thread1before wait run thread inner.
     * Run Thread2before wait run thread inner.
     * exit: Wait Thread wait thread.
     * Run Thread2after wait run thread inner.
     * Run Thread1after wait run thread inner.
     */
    public static void main(String[] args) throws Exception {
        WaitThread waitRunner = new WaitThread();
        Thread waitThread = new Thread(waitRunner, "Wait Thread");

        waitThread.start();

        RunThread runRunner1 = new RunThread(waitThread);
        RunThread runRunner2 = new RunThread(waitThread);

        Thread runThread1 = new Thread(runRunner1, "Run Thread1");
        Thread runThread2 = new Thread(runRunner2, "Run Thread2");

        runThread1.start();
        runThread2.start();
    }

    static class WaitThread implements Runnable {
        @Override
        public void run() {
            long t1 = System.currentTimeMillis();
            System.out.println(Thread.currentThread().getName() + " wait thread.");
            while (true) {
                long t2 = System.currentTimeMillis();
                if (t2 - t1 > 10 * 1000) {
                    break;
                }
            }
            System.out.println("exit: " + Thread.currentThread().getName() + " wait thread.");
        }
    }

    static class RunThread implements Runnable {
        private final Thread thread;

        public RunThread(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " run thread outer.");
            synchronized (thread) {
                System.out.println(Thread.currentThread().getName() + "before wait run thread inner.");
                try {
                    thread.wait(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "after wait run thread inner.");
            }
        }
    }
}
複製代碼
  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()操做,也不會喚醒threadA,由於鎖的對象都不同。一個是threadB的實例,一個是線程組的實例。

等待/通知的經典範式

能夠發現Thread.join()方法與等待/通知的經典範式中的等待範式一模一樣。 而Thread.exit()方法則有點相似於其中的通知範式。

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

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

通知方遵循以下原則:

  1. 得到對象的鎖。
  2. 改變條件。
  3. 通知全部等待在對象上的線程。 對應的僞代碼以下:
synchronized(對象) {
	改變條件
	對象.notifyAll();
}
複製代碼

最後,以爲寫的不錯的同窗麻煩點個贊,支持一下唄^_^~

參考與感謝

  1. 《Java併發編程的藝術》
  2. stackoverflow.com/questions/9…
  3. www.jianshu.com/p/81a56497e…
  4. www.cnblogs.com/zhengyun_us…
相關文章
相關標籤/搜索