前言
在日常编码的过程中,我们经常会使用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方法:
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:
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产生的源头
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方法完成的,该对象内部的原理后面统一说
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方法:
public static void park() {
UNSAFE.park(false, 0L);
}
Unsafe.java
查看Unsafe.park,可以看到是一个native方法
public native void park