03.關於執行緒你必須知道的8個問題(中)

王有志發表於2022-12-26

大家好,我是王有志。

原計劃是今天結束執行緒的部分,但是寫完後才發現,光Thread類的核心方法分析就寫了5000多字了,所以不得不再拆出來一篇。

02.關於執行緒你必須知道的8個問題(上)我們一起學習瞭如何建立執行緒,以及Java中執行緒狀態,那麼今天就來學習Thread類的核心方法。

Tips

  • Java及JVM原始碼基於Java 11
  • JVM原始碼僅展示關鍵內容,另附Open JDK連結
  • 文末附Java方法使用Demo的Gitee地址

Thread.start和Thread.run

上一篇中我們已經知道,Thread.run實際上是來自Runnable介面,直接呼叫並不會啟動新執行緒,只會在主執行緒中執行。

Thread.start方法中呼叫的Thread.start0方法是真正承載了建立執行緒,呼叫Thread.run方法的能力

其實到這裡已經回答了它們之間的區別,接下來我們一起來看底層是如何實現的。

Tips:有物件導向程式語言基礎的,看懂JVM原始碼對你來說並不困難。

首先是thread.c檔案,該檔案為Java中Thread類註冊了native方法。

static JNINativeMethod methods[] = {
    {"start0",          "()V",        (void *)&JVM_StartThread},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",           "(J)V",       (void *)&JVM_Sleep},
    {"interrupt0",     "()V",        (void *)&JVM_Interrupt}
};

Tips:native方法是Java Native Interface,簡稱JNI。

第一眼就可以看到start0對應的JVM方法JVM_StartThread,實現是在jvm.cpp中:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
	if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
		throw_illegal_thread_state = true;
	} else {
		// 建立虛擬機器層面的執行緒
		native_thread = new JavaThread(&thread_entry, sz);
	}
	
	Thread::start(native_thread);
JVM_END

接著來看new JavaThread做了什麼,在thread.cpp中:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
	os::create_thread(this, thr_type, stack_sz);
}

os::create_thread建立了作業系統層面的執行緒。這和上一篇中得到的結論是一致的,Java中的Thread.start0完成了作業系統層面執行緒的建立和啟動

個人認為Thread.runThread.start是沒什麼可比性的。如果被問到這個問題,要麼是面試官懶,網上隨便找找就來問,要麼是技術水平確實一般。

Tips

  • Thread::start方法在thread.cpp
  • os::create_thread方法在os_linux.cpp中,注意作業系統的區別
  • os::pd_start_thread方法在os_linux.cpp中,注意作業系統的區別
  • os::start_thread方法在os.cpp

Thread.sleep和Object.wait

接下來看兩個可以放在一起比較的方法:

  • Object.wait
  • Thread.sleep

很明顯的區別是,它們並不在同一個類中定義,其次方法名上也能看出些許差別,“等待”和“睡眠”。

Object.wait

Java在Object類中,提供了2個wait方法的過載,不過最終都是呼叫JNI方法:

public final native void wait(long timeoutMillis) throws InterruptedException;

方法宣告中我們能得知該方法的作用--使執行緒暫停指定的時間

接著我們來看Object.wait的方法註釋:

Causes the current thread to wait until it is awakened, typically by being notified or interrupted, or until a certain amount of real time has elapsed.

使當前執行緒阻塞,直到主動喚醒或者超過指定時間。清晰的說明了Object.wait的功能,另外也提示瞭如何喚醒執行緒:

  • Object.notify
  • Object.notifyAll

有了之前的經驗,很容易想到Object.wait方法是在Object.c中註冊的。我們找到它在jvm.cpp中的實現:

JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))
	ObjectSynchronizer::wait(obj, ms, CHECK);
JVM_END

接著是ObjectSynchronizer::wait,在synchronizer.cpp中:

int ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_wait);
  monitor->wait(millis, true, THREAD);
  return dtrace_waited_probe(monitor, obj, THREAD);
}

獲取ObjectMonitor物件時,呼叫了ObjectSynchronizer::inflate方法,inflate翻譯過來是膨脹的意思,是鎖膨脹的過程。實際上,在未展示的程式碼中,還有偏向鎖的過程,不過這些不是這部分的重點。

然後呼叫ObjectMonitor.wait,這個方法有225行,只看想要的部分:

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
	// 獲取當前執行緒
	Thread * const Self = THREAD;
	// 新增到等待佇列中
	AddWaiter(&node);
	// 退出監視器
	exit(true, Self);
	// 對等待時間的處理
	if (millis <= 0) {
		Self->_ParkEvent->park();
	} else {
		ret = Self->_ParkEvent->park(millis);
	}
}

答案已經呼之欲出了,ObjectMonitor.wait中退出了監視器,在Java層面就是Object.wait方法會釋放監視器鎖

對不同等待時間的處理也需要關注一下,millis <= 0的情況下,執行的是Self->_ParkEvent->park(),除非主動喚醒,否則執行緒永遠停在這裡。在Java層面看,執行object.wait(0)會使當前執行緒永久阻塞

既然都到這了,就多說一句,ObjectMonitor.exit中有幾行關鍵程式碼,是synchronized特性實現的關鍵:

void ObjectMonitor::exit(bool not_suspended, TRAPS) {
	for (;;) {
		if (Knob_ExitPolicy == 0) {
			OrderAccess::release_store(&_owner, (void*)NULL);
			OrderAccess::storeload();
		}
	}
}

這些內容我們提前混個眼熟,後面在synchronized中詳細解釋。

我們來思考兩個問題:

  • 為什麼Object.wait必須要在synchronized中呼叫?
  • 為什麼wait方法設計在Object類中,而不是Thread類中?

首先,我們已經知道Object.wait的底層實現中,要釋放監視器鎖,釋放的前提是什麼?要先擁有監視器鎖。那麼在synchronized中呼叫Object.wait就很容易理解了。

其次,鎖住的是什麼?是物件,從來都不是執行執行緒(Thread例項是執行緒物件,不是執行執行緒)。因此涉及到監視器鎖操作的方法是不是放到Object中更合適呢?

最後,如果你仔細閱讀過Object.wait所有過載方法註釋的話,你會發現一個詞:spurious wakeup(虛假喚醒)

這是沒有主動notify/notifyAll,或者被動中斷,超時的情況下就喚醒處於WAITING狀態的執行緒。因此Java也建議你在迴圈中呼叫Object.wait

synchronized (obj) {
	while (<condition does not hold> and <timeout not exceeded>) {
	long timeoutMillis = ... ; // recompute timeout values
	int nanos = ... ;
	obj.wait(timeoutMillis, nanos);
  }
  ...// Perform action appropriate to condition or timeout
}

簡單解釋下虛假喚醒產生的原因,我們已經知道Object.wait最終是透過Self->_ParkEvent->park()Self->_ParkEvent->park(millis)實現執行緒暫停的,其呼叫的park方法位於os_posix.cpp中:

void os::PlatformEvent::park() {
	status = pthread_cond_wait(_cond, _mutex);
}

int os::PlatformEvent::park(jlong millis) {
	status = pthread_cond_timedwait(_cond, _mutex, &abst);
}

pthread_cond_waitpthread_cond_timedwait是Linux對POSIX的實現,知道其作用即可,就不繼續深入了。

我們很容易聯想到,Object.notify的底層實現是呼叫os::PlatformEvent::unpark方法完成的。不出所料,從Object.c到ObjectMonitor.cpp,最後會發現該方法包含在os_posix.cpp中:

void os::PlatformEvent::unpark() {
	status = pthread_cond_signal(_cond);
}

同樣的,pthread_cond_signal也是Linux對POSIX的實現。Linux man page中對其的解釋是:

The pthread_cond_broadcast() function shall unblock all threads currently blocked on the specified condition variable cond.
The pthread_cond_signal_() function shall unblock at least one of the threads that are blocked on the specified condition variable cond (if any threads are blocked on cond).

其中第二段是關鍵,即pthread_cond_signal喚醒至少一個阻塞在指定條件上的執行緒。也就是說,呼叫Object.notify可能會喚醒不止一個符合條件的執行緒。

Java層面有一個經典的例子--生產者消費者,只貼出產品部分的程式碼(全量請檢視Gitee):

static class Product {
    private int count;
    private synchronized void increment() throws InterruptedException {
        if (this.count > 0) {
            wait();
        }
        count++;
        System.out.println(Thread.currentThread().getName() + "生產,總數:" + this.count);
        notify();
    }
    
    private synchronized void decrement() throws InterruptedException {
        if (this.count <= 0) {
            wait();
        }
        count--;
        System.out.println(Thread.currentThread().getName() + "消費,總數:" + this.count);
        notify();
    }
}

如果有1個生產者,多個消費者,消費者判定產品數量為0後,全部進入等待,生產者生產後,通知消費者消費,此時多個消費者被喚醒,直接進行消費,造成產品的總量為負數的情況。

改進的方法也很簡單:

  • 判斷方式由if修改為while,不斷地檢查條件
  • notify修改為notifyAll,避免死鎖產生

Thread.sleep

首先是方法宣告:

public static native void sleep(long millis) throws InterruptedException;

透過字面意思可以看出,讓執行緒“睡眠”指定時間。再來看註釋提供了哪些資訊:

Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds plus the specified number of nanoseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.

最後一句非常重要,The thread does not lose ownership of any monitors意思是,使執行緒進入休眠,但不會丟失任何監視器鎖的所有權。通俗點來說就是,我可以不用,但我不能沒有。

Thread.sleep依舊是JNI方法,直接看JVM實現,在jvm.cpp中:

JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
  HOTSPOT_THREAD_SLEEP_BEGIN(millis);
  EventThreadSleep event;
  if (millis == 0) {
    os::naked_yield();
  } else {
    ThreadState old_state = thread->osthread()->get_state();
    thread->osthread()->set_state(SLEEPING);
    if (os::sleep(thread, millis, true) == OS_INTRPT) {
      if (!HAS_PENDING_EXCEPTION) {
        if (event.should_commit()) {
          post_thread_sleep_event(&event, millis);
        }
        HOTSPOT_THREAD_SLEEP_END(1);
      }
    }
    thread->osthread()->set_state(old_state);
  }
  HOTSPOT_THREAD_SLEEP_END(0);
JVM_END

判斷休眠時間millis,如果millis == 0,呼叫os::naked_yield(),原始碼在os_linux.cpp中,該方法會讓出CPU時間。真是“大公無私”啊,但是喚醒是由作業系統決定

Tips:Java 11對millis == 0的邏輯做了修改,可以檢視Java 8的邏輯,我有點忘了。

也就是說,執行thread.sleep(0)並不是“咻”的一下什麼都不做就結束了,而是真正的讓出了CPU時間

接著是else的部分,最關鍵的是os::sleep(thread, millis, true) ,呼叫作業系統sleep方法進入休眠,以對Linux的封裝os_posix.cpp中的實現為例:

int os::sleep(Thread* thread, jlong millis, bool interruptible) {
  ParkEvent * const slp = thread->_SleepEvent ;
  jlong prevtime = javaTimeNanos();
  for (;;) {
    jlong newtime = javaTimeNanos();
    millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC;
    if (millis <= 0) {
      return OS_OK;
    }
    prevtime = newtime;
    slp->park(millis);
  }
}

簡化後就很好理解了,計算millis剩餘時間,millis > 0呼叫park暫停執行緒,喚醒後繼續迴圈,millis <= 0則表示休眠結束。

到這裡Thread.sleep的內容也算告一段落了,分析的過程中沒有發現涉及到ObjectMontior的地方,因此斷定Thread.sleep並不會釋放監視器鎖的所有權

Thread.yield和LockSupport.park

趁熱打鐵,來看同樣擁有“暫停”能力的兩個方法:

  • Thread.yield
  • LockSupport.park

Thread.yield

首先是方法宣告:

public static native void yield();

還是熟悉的JNI方法。同樣從註釋開始:

A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.

這句話很好理解,提示排程器當前執行緒可以放棄處理器時間,但是排程器可以忽略

直接來看JVM實現:

JVM_ENTRY(void, JVM_Yield(JNIEnv *env, jclass threadClass))
  if (os::dont_yield()) {
	  return;
  }
  os::naked_yield();
JVM_END

是不是很熟悉?和我們在Thread.sleep中看到millis == 0的場景不能說相似吧,簡直是一模一樣。

強調一下,Thread.yield只是暫時讓出CPU時間,並不是不再執行,也沒有釋放監視器鎖

LockSupport.park

LockSupport.park常常會和Thread.sleepThread.yield以及Object.wait一起比較,趁這次一起說完。

從Java原始碼入手:

private static final Unsafe U = Unsafe.getUnsafe();

public static void park() {
    U.park(false, 0L);
}

好傢伙!!!LockSupport啥也不幹,直接使用大名鼎鼎的Unsafe,那麼直接分析Unsafe.park

在此之前,還是要先看註釋:

Disables the current thread for thread scheduling purposes unless the permit is available.

翻譯過來就是,未獲得許可的情況下,一直暫停執行緒。從表象上看和Object.wait很相似,但是別忘了Object.wait會釋放監視器鎖。

Unsafe.park

依舊是方法宣告:

@HotSpotIntrinsicCandidate
public native void park(boolean isAbsolute, long time);

Tips@HotSpotIntrinsicCandidate是Java 9中引入的,表示方法在HotSpot虛擬機器中有高效的實現。

Unsfae.java的方法是直接在unsafe.cpp中註冊的,實現也在unsafe.cpp中:

UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
  thread->parker()->park(isAbsolute != 0, time);
} UNSAFE_END

需要注意,Thread.sleep中使用的是os::PlatformEvent::park,這裡呼叫的是Parker::park,在os_posix.cpp中:

void Parker::park(bool isAbsolute, jlong time) {
  if (time == 0) {
    _cur_index = REL_INDEX;
    status = pthread_cond_wait(&_cond[_cur_index], _mutex);
  } else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
  }
}

Parker::park提供了兩種場景,暫停指定時間依賴於pthread_cond_timedwait實現,對應LockSupport.parkNanos,不限時暫停依賴於pthread_cond_wait實現,對應LockSupport.park

從原始碼來看,Thread.sleep中使用的os::PlatformEvent::park是簡化版的Parker::park。另外,我們也可以得到一個隱藏結論:LockSupport.park並不會釋放監視器鎖

Thread.join

先來看Java中關於join(long millis)的註釋:

Waits at most millis milliseconds for this thread to die. A timeout of 0 means to wait forever.

比較容易翻譯,等待指定的時間,或呼叫執行緒執行結束。如果指定時間為0,則會永遠等待

看起來又是關於執行緒“暫停”的方法了,我們來看原始碼:

public final synchronized void join(final long millis) throws InterruptedException {  
    if (millis > 0) {  
        if (isAlive()) {  
            final long startTime = System.nanoTime();  
            long delay = millis;  
            do {  
                wait(delay);  
            } while (isAlive() && (delay = millis -  TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);  
        }  
    } else if (millis == 0) {  
        while (isAlive()) {  
            wait(0);  
        }  
    } else {  
        throw new IllegalArgumentException("timeout value is negative");  
    }  
}

邏輯很清晰,也沒有呼叫太多JNI方法。看起來歲月靜好,不過,我們先寫一段測試程式碼:

public class JoinThread extends Thread{
    private Thread joinThread;
    @Override
    public void run() {
        System.out.println("[join測試]執行緒:[" + Thread.currentThread().getName() + "]進入!");
        if(this.joinThread != null) {
            System.out.println("[join測試]執行緒:[" + Thread.currentThread().getName() + "]準備執行join!");
            try {
                this.joinThread.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("[join測試]執行緒:[" + Thread.currentThread().getName() + "]結束!");
    }
}
public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("[join測試]執行緒:[" + Thread.currentThread().getName() + "]執行!");
        Thread t1 = new JoinThread();
        Thread t2 = new JoinThread(t1);
        t1.start();
        t2.start();
        System.out.println("[join測試]執行緒:[" + Thread.currentThread().getName() + "]結束!");
    }
}

現在我們提出兩個問題:

  • 誰在等待t1執行結束?
  • 什麼時候喚醒的執行緒?

對於第一個問題,我們先來回顧下Object.wait的使用。ObjectMonitor::wait中呼叫os::PlatformEvent::park,操作的物件是當前執行執行緒,而不是呼叫物件。

Tips:這裡有些繞,this.joinThread.join()的呼叫中,this.joinThread是執行緒物件,而不是執行執行緒,執行執行緒是Thread例項物件在作業系統層面的對映。

網上很多答案說,join方法阻塞的是主執行緒並不準確,個人理解在哪個執行緒中執行join方法(不是呼叫!!!),就阻塞哪個執行緒。舉個例子:

Thread t1 = new Thread(() -> {
	System.out.println("執行緒t1執行!");
});
  
Thread t2 = new Thread(() -> {
    try {
        t1.join();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("執行緒t2執行!");
});
  
t1.start();
t2.start();

這種情況下被阻塞的是執行緒例項物件t2在作業系統層面對映的執行執行緒。

接著我們來看第二個問題,在Thread.join的原始碼中,我們並沒有看到notify/notifyAll方法,那麼執行緒怎麼被喚醒的呢?

這裡直接給出答案,以上面的程式碼為例子,線上程t1執行結束時,JVM會喚醒等待的執行緒。也就是JVM層面執行JavaThread::exit時喚醒執行緒,原始碼在thread.cpp中:

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

透過JVM的註釋也能看出這個方法做了什麼,不過我們還是一起來看ensure_join方法,在thread.cpp中:

static void ensure_join(JavaThread* thread) {
	ObjectLocker lock(threadObj, thread);
	thread->clear_pending_exception();
	java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
	java_lang_Thread::set_thread(threadObj(), NULL);
	lock.notify_all(thread);
}

程式碼最後一行,呼叫了notify_all喚醒了所有執行緒,也就是說,此刻所有呼叫Object.wait的方法都會被喚醒。另外也可以看到,執行緒狀態被標記為TERMINATED也是在這個方法中完成的

到此為止,Thread.join的原理也已經說完了,它的本質就是呼叫Object.wait實現阻塞,因此Java的註釋中也會建議不要使用wait/notify/notifyAll:

It is recommended that applications not use wait, notify, or notifyAll on Thread instances.

Thread.interrupt

從關鍵程式碼開始:

public void interrupt() {
    interrupt0();
}

如果沒猜錯的話,interrupt0依舊是JNI方法:

private native void interrupt0();

往下追之前,來看註釋:

Interrupts this thread.

簡明扼要,中斷執行緒

這時候相信你已經能夠熟練的點開jvm.cpp了檢視原始碼了:

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  ThreadsListHandle tlh(thread);
  JavaThread* receiver = NULL;
  bool is_alive = tlh.cv_internal_thread_to_JavaThread(jthread, &receiver, NULL);
  if (is_alive) {
    Thread::interrupt(receiver);
  }
JVM_END

跳過thread.cpp,直接來到os::is_interrupted方法,在os_posix.cpp中:

void os::interrupt(Thread* thread) {
	OSThread* osthread = thread->osthread();
	if (!osthread->interrupted()) {
		// 標記執行緒為中斷狀態
		osthread->set_interrupted(true);
		OrderAccess::fence();
		// 喚醒_SleepEvent上的執行緒
		ParkEvent * const slp = thread->_SleepEvent ;
		if (slp != NULL)
			slp->unpark() ;
	}
	// 喚醒Parker上的執行緒
	if (thread->is_Java_thread())
		((JavaThread*)thread)->parker()->unpark();

	// 喚醒_ParkEvent上的執行緒
	ParkEvent * ev = thread->_ParkEvent ;
	if (ev != NULL) 
		ev->unpark() ;
}

呼叫Thread.interrupt在JVM層面並沒有立即停止執行緒,僅標記了中斷狀態,隨後嘗試喚醒處於sleep/wait/park的執行緒,真正的中斷是從作業系統獲取該執行緒的中斷狀態開始的。

結語

今天我們一起了解了Thread類中的6個方法,另外也學習了Object.waitObject.notifyLockSupport.parkUnsafe.park,雖然沒有提及Object.notifyAll,但它的原理和Object.notify完全一樣,只不過多了一層迴圈。

最後我們再透過一張表格,來對比下執行緒“暫停”方法:

當然了,“暫停”的方式不僅僅有這些,還有一些會在JUC中涉及。

本篇文章程式碼倉庫:Thread核心方法


好了,今天就到這裡了,Bye~~

相關文章