分析JVM源碼之Thread.interrupt系統級別線程打斷
一、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究竟做瞭什麼。
二、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中看源碼。
首先我們還是需要下載open-jdk的源碼,包括jdk和hotspot(jvm)
下載地址:http://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方法喚醒線程
三、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()方法底層其實是調用系統層面的喚醒條件變量達到喚醒線程的目的
四、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]); } ... }
五、利用jni實現一個可以被打斷的MyThread類
結合上一篇文章,我們利用jni實現一個自己可以被打斷的簡易MyThread類
首先定義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.通過實現一個自己的可以打斷的線程類更好地理解線程打斷的本質
以上就是分析JVM源碼之Thread.interrupt系統級別線程打斷的詳細內容,更多關於JVM Thread.interrupt 系統級別線程打斷的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Java多線程之Park和Unpark原理
- 手寫Java LockSupport的示例代碼
- Java單線程會導致死鎖你知道嗎
- C++單例模式實現線程池的示例代碼
- 超詳細講解Linux C++多線程同步的方式