JNI-從jvm源碼分析Thread.interrupt的系統級別執行緒打斷原理
前言
在java編程中,我們經常會調用Thread.sleep()方法使得執行緒停止運行一段時間,而Thread類中也提供了interrupt方法供我們去主動打斷一個執行緒。那麼執行緒掛起和打斷的本質究竟是什麼,本文就此問題作一個探究。
本文主要分為以下幾個部分
1.interrupt的使用特點
2.jvm層面上interrupt方法的本質
3.ParkEvent對象的本質
4.Park()對象的本質
5.利用jni實現一個可以被打斷的MyThread類
1.interrupt的使用特點
我們先看2個執行緒打斷的示例
首先是可打斷的情況:
@Test
public void interruptedTest() throws InterruptedException {
Thread sleep = new Thread(() -> {
try {
log.info("sleep thread start");
TimeUnit.SECONDS.sleep(1);
log.info("sleep thread end");
} catch (InterruptedException e) {
log.info("sleep thread interrupted");
}
}, "sleep_thread");
sleep.start();
TimeUnit.MILLISECONDS.sleep(100);
log.info("ready to interrupt sleep");
sleep.interrupt();
}
我們創建了一個「sleep」執行緒,其中調用了會拋出InterruptedException異常的sleep方法。「sleep」執行緒啟動100毫秒後,主執行緒調用其打斷方法,此時輸出如下:
09:50:39.312 [sleep_thread] INFO cn.tera.thread.ThreadTest - sleep thread start
09:50:39.412 [main] INFO cn.tera.thread.ThreadTest - ready to interrupt sleep
09:50:39.412 [sleep_thread] INFO cn.tera.thread.ThreadTest - sleep thread interrupted
可以看到「sleep」執行緒被打斷後,拋出了InterruptedException異常,並直接進入了catch的邏輯。
接著我們看一個不可打斷的情況:
@Test
public void normalTest() throws InterruptedException {
Thread normal = new Thread(() -> {
log.info("normal thread start");
int i = 0;
while (true) {
i++;
}
}, "normal_thread");
normal.start();
TimeUnit.MILLISECONDS.sleep(100);
log.info("ready to interrupt normal");
normal.interrupt();
}
我們創建了一個「normal」執行緒,其中是一個死循環對i++,此時輸出如下:
10:09:20.237 [normal_thread] INFO cn.tera.thread.ThreadTest - normal thread start
10:09:20.338 [main] INFO cn.tera.thread.ThreadTest - ready to interrupt normal
可以看到「normal」執行緒被打斷後,並不會拋出異常,且會繼續執行業務流程。
所以打斷執行緒並非是任何時候都會生效的,那麼我們就需要探究下interrupt究竟做了什麼。
2.jvm層面上interrupt方法的本質
Thread.java
查看interrupt方法,其中的interrupt0()正是打斷的主要方法
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
//打斷的主要方法,該方法的主要作用是設置一個打斷標記
interrupt0();
b.interrupt(this);
return;
}
}
interrupt0();
}
查看interrupt0()方法:
private native void interrupt0();
因為interrupt0()是一個本地方法,所以要了解其的究竟做了什麼,我們就需要深入到jvm中看源碼。其中涉及到了jni相關的知識,有興趣的同學可以參看我之前寫的jni基礎應用的文章。
JNI-從jvm源碼分析Thread.start的調用與Thread.run的回調
首先我們還是需要下載open-jdk的源碼,包括jdk和hotspot(jvm)
下載地址://hg.openjdk.java.net/jdk8
因為C和C++的程式碼對於java程式設計師來說比較晦澀難懂,所以在下方展示源碼的時候我只會貼出我們關心的重點程式碼,其餘的部分就省略了。
查看Thread.c:jdk源碼目錄src/java.base/share/native/libjava
找到如下程式碼:
static JNINativeMethod methods[] = {
...
{"interrupt0", "()V", (void *)&JVM_Interrupt}
...
};
可以看到interrupt0對應的jvm方法是JVM_Interrupt
查看jvm.cpp,hotspot目錄src/share/vm/prims
可以找到JVM_Interrupt方法的實現,這個方法挺簡單的:
JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_Interrupt");
...
if (thr != NULL) {
//執行執行緒打斷操作
Thread::interrupt(thr);
}
JVM_END
查看thread.cpp,hotspot目錄src/share/vm/runtime
找到interrupt方法:
void Thread::interrupt(Thread* thread) {
//執行os層面的打斷
os::interrupt(thread);
}
查看os_posix.cpp,hotspot目錄src/os/posix/vm
找到interrupt方法,這個方法正是打斷的重點:
void os::interrupt(Thread* thread) {
...
//獲得c++執行緒對應的系統執行緒
OSThread* osthread = thread->osthread();
//如果系統執行緒的打斷標記是false,意味著還未被打斷
if (!osthread->interrupted()) {
//將系統執行緒的打斷標記設為true
osthread->set_interrupted(true);
//這個涉及到記憶體屏障,本文不展開
OrderAccess::fence();
//這裡獲取一個_SleepEvent,並調用其unpark()方法
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}
//這裡依據JSR166標準,即使打斷標記為true,依然要調用下面的2個unpark
if (thread->is_Java_thread())
//如果是一個java執行緒,這裡獲取一個parker對象,並調用其unpark()方法
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
//這裡獲取一個_ParkEvent,並調用其unpark()方法
if (ev != NULL) ev->unpark() ;
}
這個方法中,首先判斷執行緒的打斷標誌,如果為false,則將其設置為true
並且調用了3個對象的unpark()方法,一會兒介紹著3個對象的作用。
總而言之,執行緒打斷的本質做了2件事情
1.將執行緒的打斷標誌設置為true
2.調用3個對象的unpark方法喚醒執行緒
3.ParkEvent對象的本質
在前面我們看到執行緒在調用interrupt方法的最底層其實是調用了thread中3個對象的unpark()方法,那麼這3個對象究竟代表了什麼呢,我們繼續探究。
首先我們先看SleepEvent和ParkEvent對象,這2個對象的類型是相同的
查看thread.cpp,hotspot目錄src/share/vm/runtime
找到SleepEvent和ParkEvent的定義,jvm已經給我們注釋了,ParkEven是供synchronized()使用,SleepEvent是供Thread.sleep使用:
ParkEvent * _ParkEvent; // for synchronized()
ParkEvent * _SleepEvent; // for Thread.sleep
查看park.hpp,hotspot目錄src/share/vm/runtime
在頭文件中能找到ParkEvent類的定義,繼承自os::PlatformEvent,是一個和系統相關的的PlatformEvent:
class ParkEvent : public os::PlatformEvent {
...
}
查看os_linux.hpp,hotspot目錄src/os/linux/vm
以linux系統為例,在頭文件中可以看到PlatformEvent的具體定義,我們只關注其中的重點:
首先是2個私有對象,一個pthread_mutex_t作業系統級別的訊號量,一個pthread_cond_t作業系統級別的條件變數,這2個變數是一個數組,長度都是1,這些在後面會看到是如何使用的
其次是定義了3個方法,park()、unpark()、park(jlong millis),控制執行緒的掛起和繼續執行
class PlatformEvent : public CHeapObj<mtInternal> {
private:
...
pthread_mutex_t _mutex[1];
pthread_cond_t _cond[1];
...
void park();
void unpark();
int park(jlong millis); // relative timed-wait only
...
};
查看os_linux.cpp,hotspot目錄src/os/linux/vm
接著我們就需要去看park和unpark方法的具體實現,並看看2個私有變數是如何被使用的
先看park()方法,這裡我們主要關注3個系統底層方法的調用
pthread_mutex_lock(_mutex):鎖住訊號量
status = pthread_cond_wait(_cond, _mutex):釋放訊號量,並在條件變數上等待
status = pthread_mutex_unlock(_mutex):釋放訊號量
void os::PlatformEvent::park() {
...
//鎖住訊號量
int status = pthread_mutex_lock(_mutex);
while (_Event < 0) {
//釋放訊號量,並在條件變數上等待
status = pthread_cond_wait(_cond, _mutex);
}
//釋放訊號量
status = pthread_mutex_unlock(_mutex);
}
這個方法其實非常好理解,就相當於:
synchronize(obj){
obj.wait();
}
或者:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
condition.wait();
lock.unlock();
park(jlong millis)方法就不展示了,區別只是調用一個接受時間參數的等待方法。
所以park()方法底層其實是調用系統層面的鎖和條件等待去掛起執行緒的
接著我們看unpark()方法,其中最重要的方法當然是
pthread_cond_signal(_cond):喚醒條件變數
void os::PlatformEvent::unpark() {
...
if (AnyWaiters != 0) {
//喚醒條件變數
status = pthread_cond_signal(_cond);
}
...
}
所以unpark()方法底層其實是調用系統層面的喚醒條件變數達到喚醒執行緒的目的
4.Park()對象的本質
看完了2個ParkEvent對象的本質,那麼接著我們還剩一個park()對象
查看thread.hpp,hotspot目錄src/share/vm/runtime
park()對象的定義如下:
public:
Parker* parker() { return _parker; }
查看park.hpp,hotspot目錄src/share/vm/runtime
可以看到,它是繼承自os::PlatformParker,和ParkEvent不同,下面可以看到,等待變數的數組長度變為了2,其中一個給相對時間使用,一個給絕對時間使用
class Parker : public os::PlatformParker {
pthread_mutex_t _mutex[1];
pthread_cond_t _cond[2]; // one for relative times and one for abs.
}
查看os_linux.cpp,hotspot目錄src/os/linux/vm
還是先看park方法的實現,這個方法其實是對ParkEvent中的park方法的改良版,不過總體的邏輯還是沒有變
最終還是調用pthread_cond_wait方法掛起執行緒
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);
}
...
}
最後看一下unpark方法,這裡需要先獲取一個正確的等待對象,然後通知即可:
void Parker::unpark() {
int status = pthread_mutex_lock(_mutex);
...
//因為在等待的時候會有2個等待對象,所以需要先獲取正確的索引
int index = _cur_index;
...
status = pthread_mutex_unlock(_mutex);
if (s < 1 && index != -1) {
//喚醒執行緒
status = pthread_cond_signal(&_cond[index]);
}
...
}
5.利用jni實現一個可以被打斷的MyThread類
結合上一篇文章,我們利用jni實現一個自己可以被打斷的簡易MyThread類
對於jni的基礎使用和Thread在jvm級別的本質可以參看上一篇文章,對下面每一步的意義都作了詳細的解釋
JNI-從jvm源碼分析Thread.start的調用與Thread.run的回調
首先定義MyThread.java
import java.util.concurrent.TimeUnit;
import java.time.LocalDateTime;
public class MyThread {
static {
//設置查找路徑為當前項目路徑
System.setProperty("java.library.path", ".");
//載入動態庫的名稱
System.loadLibrary("MyThread");
}
public native void startAndPark();
public native void interrupt();
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
//啟動執行緒列印一段文字,並睡眠
thread.startAndPark();
//1秒後主執行緒打斷子執行緒
TimeUnit.MILLISECONDS.sleep(1000);
System.out.println(LocalDateTime.now() + ":Main---準備打斷執行緒");
//打斷子執行緒
thread.interrupt();
System.out.println(LocalDateTime.now() + ":Main---打斷完成");
}
}
執行命令編譯MyThread.class文件並生成MyThread.h頭文件
javac -h . MyThread.java
創建MyThread.c文件
當java程式碼調用startAndPark()方法的時候,創建了一個系統級別的執行緒,並調用pthread_cond_wait進行休眠
當java程式碼調用interrupt()方法的時候,會喚醒休眠中的執行緒
#include <pthread.h>
#include <stdio.h>
#include "MyThread.h"
#include "time.h"
pthread_t pid;
pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t _cond = PTHREAD_COND_INITIALIZER;
//列印時間
void printTime(){
char strTm[50] = { 0 };
time_t currentTm;
time(¤tTm);
strftime(strTm, sizeof(strTm), "%x %X", localtime(¤tTm));
puts(strTm);
}
//子執行緒執行的方法
void* thread_entity(void* arg){
printTime();
printf("MyThread---啟動\n");
printTime();
printf("MyThread---準備休眠\n");
//阻塞執行緒,等待喚醒
pthread_cond_wait(&_cond, &_mutex);
printTime();
printf("MyThread---休眠被打斷\n");
}
//對應MyThread中的startAndPark方法
JNIEXPORT void JNICALL Java_MyThread_startAndPark(JNIEnv *env, jobject c1){
//創建一個子執行緒
pthread_create(&pid, NULL, thread_entity, NULL);
}
//對應MyThread中的interrupt方法
JNIEXPORT void JNICALL Java_MyThread_interrupt(JNIEnv *env, jobject c1){
//喚醒執行緒
pthread_cond_signal(&_cond);
}
執行命令創建動態鏈接庫
gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include MyThread.c -o libMyThread.jnilib
執行java的main方法,得到結果
子執行緒啟動後進入睡眠,主執行緒1秒鐘後打斷子執行緒,完全符合我們的預期
2020/11/13 19時42分57秒
MyThread---啟動
2020/11/13 19時42分57秒
MyThread---準備休眠
2020-11-13T19:42:58.891:Main---準備打斷執行緒
2020/11/13 19時42分58秒
MyThread---休眠被打斷
2020-11-13T19:42:58.891:Main---打斷完成
最後總結一下本文的內容
1.執行緒打斷的本質做了2件事情:設置執行緒的打斷標記,並調用執行緒3個Park對象的unpark()方法喚醒執行緒
2.執行緒掛起的本質是調用系統級別的pthread_cond_wait方法,使得等待在一個條件變數上
3.執行緒喚醒的本質是調用系統級別的pthread_cond_signal方法,喚醒等待的執行緒
4.通過實現一個自己的可以打斷的執行緒類更好地理解執行緒打斷的本質