讓面試官心服口服:Thread.sleep、synchronized、LockSupport.park的執行緒阻塞有何區別?
阿新 • • 發佈:2020-11-22
### **前言**
在日常編碼的過程中,我們經常會使用Thread.sleep、LockSupport.park()主動阻塞執行緒,或者使用synchronized和Object.wait來阻塞執行緒保證併發安全。此時我們會發現,對於Thread.sleep和Object.wait方法是會丟擲InterruptedException,而LockSupport.park()和synchronized則不會。而當我們呼叫Thread.interrupt方法時,除了synchronized,其他執行緒阻塞的方式都會被喚醒。
於是本文就來探究一下**Thread.sleep、LockSupport.park()、synchronized和Object.wait**的執行緒阻塞的原理以及**InterruptedException**的本質
本文主要分為以下幾個部分
**1.Thread.sleep的原理**
**2.LockSupport.park()的原理**
**3.synchronized執行緒阻塞的原理**
**4.ParkEvent和parker物件的原理**
**5.Thread.interrupt的原理**
**6.對於synchronized打斷原理的擴充套件**
## 1.Thread.sleep的原理
**Thread.java**
首先還是從java入手,檢視sleep方法,可以發現它直接就是一個native方法:
```java
public static native void sleep(long millis) throws InterruptedException;
```
為了檢視native方法的具體邏輯,我們就需要下載openjdk和hotspot的原始碼了,下載地址:http://hg.openjdk.java.net/jdk8
**檢視Thread.c:jdk原始碼目錄src/java.base/share/native/libjava**
可以看到對應的jvm方法是JVM_Sleep:
```c++
static JNINativeMethod methods[] = {
...
{"sleep", "(J)V", (void *)&JVM_Sleep},
...
};
```
**檢視jvm.cpp,hotspot目錄src/share/vm/prims**
找到**JVM_Sleep**方法,我們關注其重點邏輯:
方法的邏輯中,首先會做2個校驗,分別是睡眠時間和執行緒的打斷標記。其實這2個數據的校驗都是可以放到java層,不過jvm的設計者將其放到了jvm的邏輯中去判斷。
如果睡眠的時間為0,那麼會呼叫系統級別的睡眠方法**os::sleep()**,睡眠時間為最小時間間隔。在睡眠之前會儲存執行緒當前的狀態,並將其設定為SLEEPING。在睡眠結束之後恢復執行緒狀態。
接著就是sleep方法的重點,如果睡眠時間不為0,同樣需要儲存和恢復執行緒的狀態,並呼叫系統級別的睡眠方法**os::sleep()**。當然睡眠的時間會變成指定的毫秒數。
**最重要的區別是,此時會判斷os::sleep()的返回值,如果是打斷狀態,那麼就會丟擲一個InterruptException!這裡其實就是InterruptException產生的源頭**
```c++
JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
JVMWrapper("JVM_Sleep");
//如果睡眠的時間小於0,則丟擲異常。這裡資料的校驗在jvm層邏輯中校驗
if (millis < 0) {
THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
}
//如果執行緒已經被打斷了,那麼也丟擲異常
if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
}
...
//這裡允許睡眠時間為0
if (millis == 0) {
...{
//獲取並儲存執行緒的舊狀態
ThreadState old_state = thread->osthread()->get_state();
//將執行緒的狀態設定為SLEEPING
thread->osthread()->set_state(SLEEPING);
//呼叫系統級別的sleep方法,此時只會睡眠最小時間間隔
os::sleep(thread, MinSleepInterval, false);
//恢復執行緒的狀態
thread->osthread()->set_state(old_state);
}
} else {
//獲取並儲存執行緒的舊狀態
ThreadState old_state = thread->osthread()->get_state();
//將執行緒的狀態設定為SLEEPING
thread->osthread()->set_state(SLEEPING);
//睡眠指定的毫秒數,並判斷返回值
if (os::sleep(thread, millis, true) == OS_INTRPT) {
...
//丟擲InterruptedException異常
THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
}
//恢復執行緒的狀態
thread->osthread()->set_state(old_state);
}
JVM_END
```
**檢視os_posix.cpp,hotspot目錄src/os/posix/vm**
我們接著檢視**os::sleep()**方法:
首先獲取執行緒的**SleepEvent**物件,這個是執行緒睡眠的關鍵
根據是否允許打斷分為2個大分支,其中邏輯大部分是相同的,區別在於允許打斷的分支中會在迴圈中額外判斷打斷標記,如果打斷標記為true,則返回打斷狀態,並在外層方法中丟擲**InterruptedException**
最終執行緒睡眠是呼叫**SleepEvent**物件的**park**方法完成的,該物件內部的原理後面統一說
```c++
int os::sleep(Thread* thread, jlong millis, bool interruptible) {
//獲取thread中的_SleepEvent物件
ParkEvent * const slp = thread->_SleepEvent ;
...
//如果是允許被打斷
if (interruptible) {
//記錄下當前時間戳,這是時間比較的基準
jlong prevtime = javaTimeNanos();
for (;;) {
//檢查打斷標記,如果打斷標記為ture,則直接返回
if (os::is_interrupted(thread, true)) {
return OS_INTRPT;
}
//執行緒被喚醒後的當前時間戳
jlong newtime = javaTimeNanos();
//睡眠毫秒數減去當前已經經過的毫秒數
millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC;
//如果小於0,那麼說明已經睡眠了足夠多的時間,直接返回
if (millis <= 0) {
return OS_OK;
}
//更新基準時間
prevtime = newtime;
//呼叫_SleepEvent物件的park方法,阻塞執行緒
slp->park(millis);
}
} else {
//如果不能打斷,除了不再返回OS_INTRPT以外,邏輯是完全相同的
for (;;) {
...
slp->park(millis);
...
}
return OS_OK ;
}
}
```
所以**Thread.sleep**的在jvm層面上是呼叫thread中**SleepEvent**物件的**park()**方法實現阻塞執行緒,在此過程中會通過判斷時間戳來決定執行緒的睡眠時間是否達到了指定的毫秒。
而**InterruptedException**的本質是一個jvm級別對打斷標記的判斷,並且jvm也提供了不可打斷的sleep邏輯。
## 2.LockSupport.park()的原理
除了我們經常使用的Thread.sleep,在jdk中還有很多時候需要阻塞執行緒時使用的是**LockSupport.park()**方法(例如ReentrantLock),接下去我們同樣需要看下**LockSupport.park()**的底層實現
**LockSupport.java**
從java程式碼入手,檢視**LockSupport.park()**方法,可以看到它直接呼叫了Usafe類中的park方法:
```java
public static void park() {
UNSAFE.park(false, 0L);
}
```
**Unsafe.java**
檢視**Unsafe.park**,可以看到是一個native方法
```java
public native void park(boolean var1, long var2);
```
**檢視unsafe.cpp,hotspot目錄src/share/vm/prims**
找到park方法,這個方法就比sleep簡單粗暴多了,直接呼叫thread中的**parker**物件的**park()**方法阻塞執行緒
```c++
UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
...
//簡單粗暴,直接呼叫thread中的parker物件的park方法阻塞執行緒
thread->parker()->park(isAbsolute != 0, time);
...
} UNSAFE_END
```
所以**LockSupport.park**方法是不會丟擲**InterruptedException**異常的。當一個執行緒呼叫**LockSupport.park**阻塞後,如果被喚醒,那麼就直接執行之後的邏輯。而對於打斷的響應則需要使用該方法的使用者在Java級別的程式碼上通過呼叫**Thread.interrupted()**判斷打斷標記自行處理。
相比而言**Thread.sleep**則設計更為複雜,除了在jvm級別上對打斷作出響應,更提供了不可被打斷的邏輯,保證呼叫該方法的執行緒一定可以阻塞指定的時間,而這個功能是**LockSupport.park**所做不到的。
## 3.synchronized執行緒阻塞的原理
再看一下synchronized線上程阻塞上的原理。synchronized本身其實都可寫幾篇文章來探討,不過本文僅關注於其執行緒阻塞部分的邏輯。
synchronized的阻塞包括2部分:
1.呼叫**synchronized(obj)**時,如果沒有搶到鎖,那麼會進入佇列等待,並阻塞執行緒。
2.獲取到鎖之後,呼叫**obj.wait()**方法進行等待,此時也會阻塞執行緒。
先來看情況一。因為這種情況並非是呼叫類中的某個方法,而是一個關鍵字,因此我們是無法從某個類檔案入手。那麼我們就需要直接檢視位元組碼了。
首先建立一個簡單的java類
```java
public class Synchronized{
public void test(){
synchronized(this){
}
}
}
```
編譯成.class檔案後,再檢視其位元組碼
```
javac Synchronized.java
javap -v Synchronized.class
```
synchronized關鍵字在位元組碼上體現為**monitorenter**和**monitorexit**指令。
```
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
...
3: monitorenter
4: aload_1
5: monitorexit
...
```
**檢視bytecodeInterpreter.cpp,hotspot目錄/src/share/vm/interpreter**
該檔案中的方法都是用來解析各種位元組碼命令的。接著我們找到**monitorenter**方法:
這個方法就是**synchronized**關鍵字的具體加鎖邏輯,十分複雜,這裡只是展示方法的入口在哪裡。
```c++
CASE(_monitorenter): {
...
}
```
**檢視objectMonitor.cpp,hotspot目錄/src/share/vm/runtime**
最終**synchronized**的執行緒阻塞邏輯是由**objectMonitor**物件負責的,所以我們直接檢視該物件的相應方法。找到**enter**方法:
跳過其中大部分邏輯,我們看到**EnterI**方法,正是在該方法中阻塞執行緒的。
```c++
void ObjectMonitor::enter(TRAPS) {
...
//阻塞執行緒
EnterI(THREAD);
...
}
```
檢視**EnterI**方法
這個方法會在一個死迴圈中嘗試獲取鎖,如果獲取失敗則呼叫當前執行緒的**ParkEvent**的**park()**方法阻塞執行緒,否則就退出迴圈
**當然特別注意的是,這個方法是在一個死迴圈中呼叫的,因此在java級別來看,synchronized是不可打斷的,執行緒會一直阻塞直到它獲取到鎖為止。**
```c++
void ObjectMonitor::EnterI(TRAPS) {
//獲取當前執行緒物件
Thread * const Self = THREAD;
...
for (;;) {
//嘗試獲取鎖
if (TryLock(Self) > 0) break;
...
//呼叫ParkEvent的park()方法阻塞執行緒
if (_Responsible == Self || (SyncFlags & 1)) {
Self->_ParkEvent->park((jlong) recheckInterval);
} else {
Self->_ParkEvent->park();
}
...
}
...
}
```
接著來看情況二:
**檢視objectMonitor.cpp,hotspot目錄/src/share/vm/runtime**
最終**Object.wait()**的執行緒阻塞邏輯也是由**objectMonitor**物件負責的,所以我們直接檢視該物件的相應方法。找到**wait**方法:
可以看到**wait()**方法中對執行緒的打斷作出了響應,並且會丟擲**InterruptedException**,這也正是java級別的**Object.wait()**方法會丟擲該異常的原因
執行緒阻塞和synchronized一樣,是由執行緒的**ParkEvent**物件的**park()**方法完成的
```c++
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
//獲取當前執行緒物件
Thread * const Self = THREAD;
//檢查是否可以打斷
if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
...
//丟擲InterruptedException
THROW(vmSymbols::java_lang_InterruptedException());
}
if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {
//如果執行緒被打斷了,那就什麼都不做
} else if (node._notified == 0) {
//呼叫ParkEvent的park()方法阻塞執行緒
if (millis <= 0) {
Self->_ParkEvent->park();
} else {
ret = Self->_ParkEvent->park(millis);
}
}
}
```
所以對於**synchronized**和**Object.wait**來說,最終都是呼叫thread中**ParkEvent**物件的**park()**方法實現執行緒阻塞的
而在java層面上synchronized本身是不響應執行緒打斷的,但是Object.wait()方法卻是會響應打斷的,區別正是在於jvm級別的邏輯處理上有所不同。
## 4.ParkEvent和parker物件的原理
**Thread.sleep、synchronized和Object.wait**底層分別是利用執行緒**SleepEvent**和**ParkEvent**物件的**park**方法實現執行緒阻塞的。因為這2個物件實際是一個型別的,因此我們就一起來看一下其park方法究竟做了什麼
**檢視thread.cpp,hotspot目錄src/share/vm/runtime**
找到**SleepEvent**和**ParkEvent**的定義,從後面的註釋就可以發現,ParkEvent就是供synchronized()使用的,而SleepEvent則是供Thread.sleep使用的:
```c++
ParkEvent * _ParkEvent; // for synchronized()
ParkEvent * _SleepEvent; // for Thread.sleep
```
**檢視park.hpp,hotspot目錄src/share/vm/runtime**
在標頭檔案中能找到ParkEvent類的定義,繼承自**os::PlatformEvent**:
```c++
class ParkEvent : public os::PlatformEvent {
...
}
```
**檢視os_linux.hpp,hotspot目錄src/os/linux/vm**
以linux系統為例,在標頭檔案中可以看到PlatformEvent的具體定義:
我們關注的重點首先是2個private的物件,一個**pthread_mutex_t,表示作業系統級別的訊號量**,一個**pthread_cond_t,表示作業系統級別的條件變數**
其次是定義了3個方法,**park()、unpark()、park(jlong millis)**,控制執行緒的阻塞和繼續執行
```c++
class PlatformEvent : public