分析JVM源碼之Thread.interrupt系統(tǒng)級(jí)別線程打斷
一、interrupt的使用特點(diǎn)
我們先看2個(gè)線程打斷的示例
首先是可打斷的情況:
@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();
}
我們創(chuàng)建了一個(gè)“sleep”線程,其中調(diào)用了會(huì)拋出InterruptedException異常的sleep方法。“sleep”線程啟動(dòng)100毫秒后,主線程調(diào)用其打斷方法,此時(shí)輸出如下:
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異常,并直接進(jìn)入了catch的邏輯。
接著我們看一個(gè)不可打斷的情況:
@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();
}
我們創(chuàng)建了一個(gè)“normal”線程,其中是一個(gè)死循環(huán)對(duì)i++,此時(shí)輸出如下:
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”線程被打斷后,并不會(huì)拋出異常,且會(huì)繼續(xù)執(zhí)行業(yè)務(wù)流程。
所以打斷線程并非是任何時(shí)候都會(huì)生效的,那么我們就需要探究下interrupt究竟做了什么。
二、jvm層面上interrupt方法的本質(zhì)
Thread.java
查看interrupt方法,其中的interrupt0()正是打斷的主要方法
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
//打斷的主要方法,該方法的主要作用是設(shè)置一個(gè)打斷標(biāo)記
interrupt0();
b.interrupt(this);
return;
}
}
interrupt0();
}
查看interrupt0()方法:
private native void interrupt0();
因?yàn)閕nterrupt0()是一個(gè)本地方法,所以要了解其的究竟做了什么,我們就需要深入到j(luò)vm中看源碼。
首先我們還是需要下載open-jdk的源碼,包括jdk和hotspot(jvm)
下載地址:http://hg.openjdk.java.net/jdk8
因?yàn)镃和C++的代碼對(duì)于java程序員來(lái)說(shuō)比較晦澀難懂,所以在下方展示源碼的時(shí)候我只會(huì)貼出我們關(guān)心的重點(diǎn)代碼,其余的部分就省略了。
查看Thread.c:jdk源碼目錄src/java.base/share/native/libjava
找到如下代碼:
static JNINativeMethod methods[] = {
...
{"interrupt0", "()V", (void *)&JVM_Interrupt}
...
};
可以看到interrupt0對(duì)應(yīng)的jvm方法是JVM_Interrupt
查看jvm.cpp,hotspot目錄src/share/vm/prims
可以找到JVM_Interrupt方法的實(shí)現(xiàn),這個(gè)方法挺簡(jiǎn)單的:
JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_Interrupt");
...
if (thr != NULL) {
//執(zhí)行線程打斷操作
Thread::interrupt(thr);
}
JVM_END
查看thread.cpp,hotspot目錄src/share/vm/runtime
找到interrupt方法:
void Thread::interrupt(Thread* thread) {
//執(zhí)行os層面的打斷
os::interrupt(thread);
}
查看os_posix.cpp,hotspot目錄src/os/posix/vm
找到interrupt方法,這個(gè)方法正是打斷的重點(diǎn):
void os::interrupt(Thread* thread) {
...
//獲得c++線程對(duì)應(yīng)的系統(tǒng)線程
OSThread* osthread = thread->osthread();
//如果系統(tǒng)線程的打斷標(biāo)記是false,意味著還未被打斷
if (!osthread->interrupted()) {
//將系統(tǒng)線程的打斷標(biāo)記設(shè)為true
osthread->set_interrupted(true);
//這個(gè)涉及到內(nèi)存屏障,本文不展開
OrderAccess::fence();
//這里獲取一個(gè)_SleepEvent,并調(diào)用其unpark()方法
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}
//這里依據(jù)JSR166標(biāo)準(zhǔn),即使打斷標(biāo)記為true,依然要調(diào)用下面的2個(gè)unpark
if (thread->is_Java_thread())
//如果是一個(gè)java線程,這里獲取一個(gè)parker對(duì)象,并調(diào)用其unpark()方法
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
//這里獲取一個(gè)_ParkEvent,并調(diào)用其unpark()方法
if (ev != NULL) ev->unpark() ;
}
這個(gè)方法中,首先判斷線程的打斷標(biāo)志,如果為false,則將其設(shè)置為true
并且調(diào)用了3個(gè)對(duì)象的unpark()方法,一會(huì)兒介紹著3個(gè)對(duì)象的作用。
總而言之,線程打斷的本質(zhì)做了2件事情
1.將線程的打斷標(biāo)志設(shè)置為true
2.調(diào)用3個(gè)對(duì)象的unpark方法喚醒線程
三、ParkEvent對(duì)象的本質(zhì)
在前面我們看到線程在調(diào)用interrupt方法的最底層其實(shí)是調(diào)用了thread中3個(gè)對(duì)象的unpark()方法,那么這3個(gè)對(duì)象究竟代表了什么呢,我們繼續(xù)探究。
首先我們先看SleepEvent和ParkEvent對(duì)象,這2個(gè)對(duì)象的類型是相同的
查看thread.cpp,hotspot目錄src/share/vm/runtime
找到SleepEvent和ParkEvent的定義,jvm已經(jīng)給我們注釋了,ParkEven是供synchronized()使用,SleepEvent是供Thread.sleep使用:
ParkEvent * _ParkEvent; // for synchronized() ParkEvent * _SleepEvent; // for Thread.sleep
查看park.hpp,hotspot目錄src/share/vm/runtime
在頭文件中能找到ParkEvent類的定義,繼承自os::PlatformEvent,是一個(gè)和系統(tǒng)相關(guān)的的PlatformEvent:
class ParkEvent : public os::PlatformEvent {
...
}
查看os_linux.hpp,hotspot目錄src/os/linux/vm
以linux系統(tǒng)為例,在頭文件中可以看到PlatformEvent的具體定義,我們只關(guān)注其中的重點(diǎn):
首先是2個(gè)私有對(duì)象,一個(gè)pthread_mutex_t操作系統(tǒng)級(jí)別的信號(hào)量,一個(gè)pthread_cond_t操作系統(tǒng)級(jí)別的條件變量,這2個(gè)變量是一個(gè)數(shù)組,長(zhǎng)度都是1,這些在后面會(huì)看到是如何使用的
其次是定義了3個(gè)方法,park()、unpark()、park(jlong millis),控制線程的掛起和繼續(xù)執(zhí)行
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方法的具體實(shí)現(xiàn),并看看2個(gè)私有變量是如何被使用的
先看park()方法,這里我們主要關(guān)注3個(gè)系統(tǒng)底層方法的調(diào)用
pthread_mutex_lock(_mutex):鎖住信號(hào)量
status = pthread_cond_wait(_cond, _mutex):釋放信號(hào)量,并在條件變量上等待
status = pthread_mutex_unlock(_mutex):釋放信號(hào)量
void os::PlatformEvent::park() {
...
//鎖住信號(hào)量
int status = pthread_mutex_lock(_mutex);
while (_Event < 0) {
//釋放信號(hào)量,并在條件變量上等待
status = pthread_cond_wait(_cond, _mutex);
}
//釋放信號(hào)量
status = pthread_mutex_unlock(_mutex);
}
這個(gè)方法其實(shí)非常好理解,就相當(dāng)于:
synchronize(obj){
obj.wait();
}
或者:
ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); lock.lock(); condition.wait(); lock.unlock();
park(jlong millis)方法就不展示了,區(qū)別只是調(diào)用一個(gè)接受時(shí)間參數(shù)的等待方法。
所以park()方法底層其實(shí)是調(diào)用系統(tǒng)層面的鎖和條件等待去掛起線程的
接著我們看unpark()方法,其中最重要的方法當(dāng)然是
pthread_cond_signal(_cond):?jiǎn)拘褩l件變量
void os::PlatformEvent::unpark() {
...
if (AnyWaiters != 0) {
//喚醒條件變量
status = pthread_cond_signal(_cond);
}
...
}
所以u(píng)npark()方法底層其實(shí)是調(diào)用系統(tǒng)層面的喚醒條件變量達(dá)到喚醒線程的目的
四、Park()對(duì)象的本質(zhì)
看完了2個(gè)ParkEvent對(duì)象的本質(zhì),那么接著我們還剩一個(gè)park()對(duì)象
查看thread.hpp,hotspot目錄src/share/vm/runtime
park()對(duì)象的定義如下:
public:
Parker* parker() { return _parker; }
查看park.hpp,hotspot目錄src/share/vm/runtime
可以看到,它是繼承自os::PlatformParker,和ParkEvent不同,下面可以看到,等待變量的數(shù)組長(zhǎng)度變?yōu)榱?,其中一個(gè)給相對(duì)時(shí)間使用,一個(gè)給絕對(duì)時(shí)間使用
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方法的實(shí)現(xiàn),這個(gè)方法其實(shí)是對(duì)ParkEvent中的park方法的改良版,不過(guò)總體的邏輯還是沒(méi)有變
最終還是調(diào)用pthread_cond_wait方法掛起線程
void Parker::park(bool isAbsolute, jlong time) {
...
if (time == 0) {
//這里是直接長(zhǎng)時(shí)間等待
_cur_index = REL_INDEX;
status = pthread_cond_wait(&_cond[_cur_index], _mutex);
} else {
//這里會(huì)根據(jù)時(shí)間是否是絕對(duì)時(shí)間,分別等待在不同的條件上
_cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
}
...
}
最后看一下unpark方法,這里需要先獲取一個(gè)正確的等待對(duì)象,然后通知即可:
void Parker::unpark() {
int status = pthread_mutex_lock(_mutex);
...
//因?yàn)樵诘却臅r(shí)候會(huì)有2個(gè)等待對(duì)象,所以需要先獲取正確的索引
int index = _cur_index;
...
status = pthread_mutex_unlock(_mutex);
if (s < 1 && index != -1) {
//喚醒線程
status = pthread_cond_signal(&_cond[index]);
}
...
}
五、利用jni實(shí)現(xiàn)一個(gè)可以被打斷的MyThread類
結(jié)合上一篇文章,我們利用jni實(shí)現(xiàn)一個(gè)自己可以被打斷的簡(jiǎn)易MyThread類
首先定義MyThread.java
import java.util.concurrent.TimeUnit;
import java.time.LocalDateTime;
public class MyThread {
static {
//設(shè)置查找路徑為當(dāng)前項(xiàng)目路徑
System.setProperty("java.library.path", ".");
//加載動(dòng)態(tài)庫(kù)的名稱
System.loadLibrary("MyThread");
}
public native void startAndPark();
public native void interrupt();
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
//啟動(dòng)線程打印一段文字,并睡眠
thread.startAndPark();
//1秒后主線程打斷子線程
TimeUnit.MILLISECONDS.sleep(1000);
System.out.println(LocalDateTime.now() + ":Main---準(zhǔn)備打斷線程");
//打斷子線程
thread.interrupt();
System.out.println(LocalDateTime.now() + ":Main---打斷完成");
}
}
執(zhí)行命令編譯MyThread.class文件并生成MyThread.h頭文件
javac -h . MyThread.java
創(chuàng)建MyThread.c文件
當(dāng)java代碼調(diào)用startAndPark()方法的時(shí)候,創(chuàng)建了一個(gè)系統(tǒng)級(jí)別的線程,并調(diào)用pthread_cond_wait進(jìn)行休眠
當(dāng)java代碼調(diào)用interrupt()方法的時(shí)候,會(huì)喚醒休眠中的線程
#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;
//打印時(shí)間
void printTime(){
char strTm[50] = { 0 };
time_t currentTm;
time(¤tTm);
strftime(strTm, sizeof(strTm), "%x %X", localtime(¤tTm));
puts(strTm);
}
//子線程執(zhí)行的方法
void* thread_entity(void* arg){
printTime();
printf("MyThread---啟動(dòng)\n");
printTime();
printf("MyThread---準(zhǔn)備休眠\(yùn)n");
//阻塞線程,等待喚醒
pthread_cond_wait(&_cond, &_mutex);
printTime();
printf("MyThread---休眠被打斷\n");
}
//對(duì)應(yīng)MyThread中的startAndPark方法
JNIEXPORT void JNICALL Java_MyThread_startAndPark(JNIEnv *env, jobject c1){
//創(chuàng)建一個(gè)子線程
pthread_create(&pid, NULL, thread_entity, NULL);
}
//對(duì)應(yīng)MyThread中的interrupt方法
JNIEXPORT void JNICALL Java_MyThread_interrupt(JNIEnv *env, jobject c1){
//喚醒線程
pthread_cond_signal(&_cond);
}
執(zhí)行命令創(chuàng)建動(dòng)態(tài)鏈接庫(kù)
gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include MyThread.c -o libMyThread.jnilib
執(zhí)行java的main方法,得到結(jié)果
子線程啟動(dòng)后進(jìn)入睡眠,主線程1秒鐘后打斷子線程,完全符合我們的預(yù)期
2020/11/13 19時(shí)42分57秒
MyThread---啟動(dòng)
2020/11/13 19時(shí)42分57秒
MyThread---準(zhǔn)備休眠
2020-11-13T19:42:58.891:Main---準(zhǔn)備打斷線程
2020/11/13 19時(shí)42分58秒
MyThread---休眠被打斷
2020-11-13T19:42:58.891:Main---打斷完成
六、總結(jié)
1.線程打斷的本質(zhì)做了2件事情:設(shè)置線程的打斷標(biāo)記,并調(diào)用線程3個(gè)Park對(duì)象的unpark()方法喚醒線程
2.線程掛起的本質(zhì)是調(diào)用系統(tǒng)級(jí)別的pthread_cond_wait方法,使得等待在一個(gè)條件變量上
3.線程喚醒的本質(zhì)是調(diào)用系統(tǒng)級(jí)別的pthread_cond_signal方法,喚醒等待的線程
4.通過(guò)實(shí)現(xiàn)一個(gè)自己的可以打斷的線程類更好地理解線程打斷的本質(zhì)
以上就是分析JVM源碼之Thread.interrupt系統(tǒng)級(jí)別線程打斷的詳細(xì)內(nèi)容,更多關(guān)于JVM Thread.interrupt 系統(tǒng)級(jí)別線程打斷的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
maven如何利用springboot的配置文件進(jìn)行多個(gè)環(huán)境的打包
這篇文章主要介紹了maven如何利用springboot的配置文件進(jìn)行多個(gè)環(huán)境的打包,在Spring Boot中多環(huán)境配置文件名需要滿足application-{profiles.active}.properties的格式,其中{profiles.active}對(duì)應(yīng)你的環(huán)境標(biāo)識(shí),本文給大家詳細(xì)講解,需要的朋友可以參考下2023-02-02
使用Springboot整合GridFS實(shí)現(xiàn)文件操作
這篇文章主要介紹了使用Springboot整合GridFS實(shí)現(xiàn)文件操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
hibernate關(guān)于session的關(guān)閉實(shí)例解析
這篇文章主要介紹了hibernate關(guān)于session的關(guān)閉實(shí)例解析,分享了相關(guān)代碼示例,小編覺(jué)得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02
Java實(shí)現(xiàn)合并兩個(gè)有序序列算法示例
這篇文章主要介紹了Java實(shí)現(xiàn)合并兩個(gè)有序序列算法,簡(jiǎn)單描述了序列合并算法的原理與java合并有序序列的具體操作步驟及相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-09-09
Java 把json對(duì)象轉(zhuǎn)成map鍵值對(duì)的方法
這篇文章主要介紹了java 把json對(duì)象中轉(zhuǎn)成map鍵值對(duì)的方法,本文的目的是把json串轉(zhuǎn)成map鍵值對(duì)存儲(chǔ),而且只存儲(chǔ)葉節(jié)點(diǎn)的數(shù)據(jù) 。需要的朋友可以參考下2018-04-04
Java中利用BitMap位圖實(shí)現(xiàn)海量級(jí)數(shù)據(jù)去重
有許多方法可以用來(lái)去重,比如使用列表、集合等等,但這些方法通常只適用于一般情況,然而,當(dāng)涉及到大量數(shù)據(jù)去重時(shí),常見的 Java Set、List,甚至是 Java 8 的新特性 Stream 流等方式就顯得不太合適了,本文給大家介紹了Java中利用BitMap位圖實(shí)現(xiàn)海量級(jí)數(shù)據(jù)去重2024-04-04

