关于文章涉及到的jdk源码,这里把最新的jdk源码分享给你们----->jdk源码html
在上篇文章《Java并发编程之锁机制之AQS(AbstractQueuedSynchronizer)》中咱们了解了整个AQS的内部结构,与其独占式与共享式获取同步状态的实现。可是并无详细描述线程是如何进行阻塞与唤醒的。我也提到了线程的这些操做都与LockSupport
工具类有关。如今咱们就一块儿来探讨一下该类的具体实现。java
了解线程的阻塞和唤醒,咱们须要查看LockSupport类。具体代码以下:linux
public class LockSupport {
private LockSupport() {} // Cannot be instantiated.
private static void setBlocker(Thread t, Object arg) {
U.putObject(t, PARKBLOCKER, arg);
}
public static void unpark(Thread thread) {
if (thread != null)
U.unpark(thread);
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, 0L);
setBlocker(t, null);
}
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, nanos);
setBlocker(t, null);
}
}
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(true, deadline);
setBlocker(t, null);
}
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return U.getObjectVolatile(t, PARKBLOCKER);
}
public static void park() {
U.park(false, 0L);
}
public static void parkNanos(long nanos) {
if (nanos > 0)
U.park(false, nanos);
}
public static void parkUntil(long deadline) {
U.park(true, deadline);
}
//省略部分代码
private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
private static final long PARKBLOCKER;
private static final long SECONDARY;
static {
try {
PARKBLOCKER = U.objectFieldOffset
(Thread.class.getDeclaredField("parkBlocker"));
SECONDARY = U.objectFieldOffset
(Thread.class.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
}
复制代码
从上面的代码中,咱们能够知道LockSupport中的对外提供的方法都是静态方法
。这些方法提供了最基本的线程阻塞和唤醒功能,在LockSupport类中定义了一组以park开头的方法用来阻塞当前线程。以及unPark(Thread thread)
方法来唤醒一个被阻塞的线程。关于park开头的方法具体描述以下表所示:编程
其中park(Object blocker)
与parkNanos(Object blocker, long nanos)
及parkUntil(Object blocker, long deadline)
三个方法是Java 6中新增长的方法。其中参数blocker是用来标识当前线程等待的对象(下文简称为阻塞对象),该对象主要用于问题排查和系统监控
。windows
因为在Java 5以前,当线程阻塞时(使用synchronized关键字)在一个对象上时,经过线程dump可以查看到该线程的阻塞对象。方便问题定位,而Java 5退出的Lock等并发工具却遗漏了这一点,导致在线程dump时没法提供阻塞对象的信息。所以,在Java 6中,LockSupport新增了含有阻塞对象的park方法。用以替代原有的park方法。bash
可能有不少读者对Blocker的原理有点好奇,既然线程都被阻塞了,是经过什么办法将阻塞对象设置到线程中去的呢?
不急不急,咱们继续查看含有阻塞对象(Object blocker)的park方法。 咱们发现内部都调用了setBlocker(Thread t, Object arg)
方法。具体代码以下所示:多线程
private static void setBlocker(Thread t, Object arg) {
U.putObject(t, PARKBLOCKER, arg);
}
复制代码
其中 U
为sun.misc.包下的Unsafe
类。而其中的PARKBLOCKER
是在静态代码块中进行赋值的,也就是以下代码:并发
private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
static {
try {
PARKBLOCKER = U.objectFieldOffset
(Thread.class.getDeclaredField("parkBlocker"));
//省略部分代码
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
复制代码
Thread.class.getDeclaredField("parkBlocker")
方法其实很好理解,就是获取线程中的parkBlocker
字段。若是有则返回其对应的Field字段,若是没有则抛出NoSuchFieldException
异常。那么关于Unsafe中的objectFieldOffset(Field f)
方法怎么理解呢?dom
在描述该方法以前,须要给你们讲一个知识点。在JVM中,能够自由选择如何实现Java对象的"布局"
,也就Java对象的各个部分分别放在内存那个地方,JVM是能够感知和决定的。 在sun.misc.Unsafe中提供了objectFieldOffset()
方法用于获取某个字段相对 Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可使用前面获取的偏移量来访问某个Java 对象的某个字段。异步
有可能你们理解起来比较困难,这里给你们画了一个图,帮助你们理解,具体以下图所示:
在上图中,咱们建立了两个Thread对象,其中Thread对象1在内存中分配的地址为0x10000-0x10100
,Thread对象2在内存中分配的地址为0x11000-0x11100
,其中parkBlocker
对应内存偏移量为2(这里咱们假设相对于其对象的“起始位置”的偏移量为2)。那么经过objectFieldOffset(Field f)
就能获取该字段的偏移量。须要注意的是某字段在其类中的内存偏移量老是相同的
,也就是对于Thread对象1与Thread对象2,parkBlocker字段在其对象所在的内存偏移量始终是相同的。
那么咱们再回到setBlocker(Thread t, Object arg)
方法,当咱们获取到parkBlocker
字段在其对象内存偏移量后, 接着会调用U.putObject(t, PARKBLOCKER, arg);
,该方法有三个参数,第一个参数是操做对象,第二个参数是内存偏移量,第三个参数是实际存储值。该方法理解起来也很简单,就是操做某个对象中某个内存地址下的数据
。那么结合咱们上面所讲的。该方法的实际操做结果以下图所示:
到如今,咱们就应该懂了,尽管当前线程已经阻塞
,可是咱们仍是能直接操控线程中实际存储该字段的内存区域
来达到咱们想要的结果。
经过阅读源代码咱们能够发现,LockSupport中关于线程的阻塞和唤醒,主要调用的是sun.misc.Unsafe 中的park(boolean isAbsolute, long time)
与unpark(Object thread)
方法,也就是以下代码:
private static final jdk.internal.misc.Unsafe theInternalUnsafe =
jdk.internal.misc.Unsafe.getUnsafe();
public void park(boolean isAbsolute, long time) {
theInternalUnsafe.park(isAbsolute, time);
}
public void unpark(Object thread) {
theInternalUnsafe.unpark(thread);
}
复制代码
查看sun.misc.包下的Unsafe.java文件咱们能够看出,内部其实调用的是jdk.internal.misc.Unsafe中的方法。继续查看jdk.internal.misc.中的Unsafe.java中对应的方法:
@HotSpotIntrinsicCandidate
public native void unpark(Object thread);
@HotSpotIntrinsicCandidate
public native void park(boolean isAbsolute, long time);
复制代码
经过查看方法,咱们能够得出最终调用的是JVM中的方法,也就是会调用hotspot.share.parims
包下的unsafe.cpp
中的方法。继续跟踪。
UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
//省略部分代码
thread->parker()->park(isAbsolute != 0, time);
//省略部分代码
} UNSAFE_END
UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread)) {
Parker* p = NULL;
//省略部分代码
if (p != NULL) {
HOTSPOT_THREAD_UNPARK((uintptr_t) p);
p->unpark();
}
} UNSAFE_END
复制代码
经过观察代码咱们发现,线程的阻塞和唤醒实际上是与hotspot.share.runtime
中的Parker类
相关。咱们继续查看:
class Parker : public os::PlatformParker {
private:
volatile int _counter ;//该变量很是重要,下文咱们会具体描述
//省略部分代码
protected:
~Parker() { ShouldNotReachHere(); }
public:
// For simplicity of interface with Java, all forms of park (indefinite,
// relative, and absolute) are multiplexed into one call.
void park(bool isAbsolute, jlong time);
void unpark();
//省略部分代码
}
复制代码
在上述代码中,volatile int _counter
该字段的值很是重要,必定要注意其用volatile修饰
(在下文中会具体描述,接着当咱们经过SourceInsight
工具(推荐你们阅读代码时,使用该工具)点击其park与unpark方法时,咱们会获得以下界面:
从图中红色矩形中咱们可也看出,针对线程的阻塞和唤醒,不一样操做系统有着不一样的实现
。众所周知Java是跨平台的。针对不一样的平台,作出不一样的处理。也是很是理解的。由于做者对windows与solaris操做系统不是特别了解。因此这里我选择对Linux下的平台下进行分析。也就是选择hotspot.os.posix
包下的os_posix.cpp
文件进行分析。
为了方便你们理解Linux下的阻塞实现,在实际代码中我省略了一些不重要的代码,具体以下图所示:
void Parker::park(bool isAbsolute, jlong time) {
//(1)若是_counter的值大于0,那么直接返回
if (Atomic::xchg(0, &_counter) > 0) return;
//获取当前线程
Thread* thread = Thread::current();
JavaThread *jt = (JavaThread *)thread;
//(2)若是当前线程已经中断,直接返回。
if (Thread::is_interrupted(thread, false)) {
return;
}
//(3)判断时间,若是时间小于0,或者在绝对时间状况下,时间为0直接返回
struct timespec absTime;
if (time < 0 || (isAbsolute && time == 0)) { // don't wait at all return; } //若是时间大于0,判断阻塞超时时间或阻塞截止日期,同时将时间赋值给absTime if (time > 0) { to_abstime(&absTime, time, isAbsolute); } //(4)若是当前线程已经中断,或者申请互斥锁失败,则直接返回 if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) { return; } //(5)若是是时间等于0,那么就直接阻塞线程, if (time == 0) { _cur_index = REL_INDEX; // arbitrary choice when not timed status = pthread_cond_wait(&_cond[_cur_index], _mutex); assert_status(status == 0, status, "cond_timedwait"); } //(6)根据absTime以前计算的时间,阻塞线程相应时间 else { _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX; status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime); assert_status(status == 0 || status == ETIMEDOUT, status, "cond_timedwait"); } //省略部分代码 //(7)当线程阻塞超时,或者到达截止日期时,直接唤醒线程 _counter = 0; status = pthread_mutex_unlock(_mutex); //省略部分代码 } 复制代码
从整个代码来看其实关于Linux下的park方法分为如下七个步骤:
Atomic::xchg
方法,将_counter
的值赋值为0,其方法的返回值为以前_counter的值
,若是返回值大于0
(由于有其余线程操做过_counter的值,也就是其余线程调用过unPark
方法),那么就直接返回。阻塞超时时间
或阻塞截止日期
,同时将时间赋值给absTime
pthread_cond_timedwait
方法阻塞线程相应的时间。pthread_mutex_unlock
方法直接唤醒线程,同时将_counter
赋值为0。由于关于Linux的阻塞涉及到其内部函数,这里将用到的函数都进行了声明。你们能够根据下表所介绍的方法进行理解。具体方法以下表所示:
在了解了Linux的park实现后,再来理解Linux的唤醒实现就很是简单了,查看相应方法:
void Parker::unpark() {
int status = pthread_mutex_lock(_mutex);
assert_status(status == 0, status, "invariant");
const int s = _counter;
//将_counter的值赋值为1
_counter = 1;
// must capture correct index before unlocking
int index = _cur_index;
status = pthread_mutex_unlock(_mutex);
assert_status(status == 0, status, "invariant");
//省略部分代码
}
复制代码
其实从代码总体逻辑来说,最终唤醒其线程的方法为pthread_mutex_unlock(_mutex)
(关于该函数的做用,我已经在上表进行介绍了。你们能够参照Linux下的park实现中的图表进行理解)。同时将_counter的值赋值为1
, 那么结合咱们上文所讲的park(将线程进行阻塞)方法,那么咱们能够得知整个线程的唤醒与阻塞,在Linux系统下,实际上是受到Parker类中的_counter的值的影响的
。
如今咱们基本了解了LockSupport的基本原理。如今咱们来看看它的基本使用吧。在例子中,为了方便你们顺便弄清blocker的做用,这里我调用了带blocker的park方法。具体代码以下所示:
class LockSupportDemo {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
LockSupport.park("线程a的blocker数据");
System.out.println("我是被线程b唤醒后的操做");
}
});
a.start();
//让当前主线程睡眠1秒,保证线程a在线程b以前执行
Thread.sleep(1000);
Thread b = new Thread(new Runnable() {
@Override
public void run() {
String before = (String) LockSupport.getBlocker(a);
System.out.println("阻塞时从线程a中获取的blocker------>" + before);
LockSupport.unpark(a);
//这里睡眠是,保证线程a已经被唤醒了
try {
Thread.sleep(1000);
String after = (String) LockSupport.getBlocker(a);
System.out.println("唤醒时从线程a中获取的blocker------>" + after);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
b.start();
}
}
复制代码
代码中,建立了两个线程,线程a与线程b(线程a优先运行与线程b),在线程a中,经过调用LockSupport.park("线程a的blocker数据");
给线程a设置了一个String类型的blocker,当线程a运行的时候,直接将线程a阻塞。在线程b中,先会获取线程a中的blocker,打印输出后。再经过LockSupport.unpark(a);
唤醒线程a。当唤醒线程a后。最后输出并打印线程a中的blocker。 实际代码运行结果以下:
阻塞时从线程a中获取的blocker------>线程a的blocker数据
我是被线程b唤醒后的操做
唤醒时从线程a中获取的blocker------>null
复制代码
从结果中,咱们能够看出,线程a被阻塞时,后续就不会再进行操做了。当线程a被线程b唤醒后。以前设置的blocker也变为null了。同时若是在线程a中park语句后还有额外的操做。那么会继续运行。关于为毛以前的blocker以前变为null,具体缘由以下:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, 0L);//当线程被阻塞时,会阻塞在这里
setBlocker(t, null);//线程被唤醒时,会将blocer置为null
}
复制代码
经过上述例子,咱们彻底知道了blocker能够在线程阻塞的时候,获取数据。也就证实了当咱们对线程进行问题排查和系统监控的时候blocker的有着很是重要的做用。
该文章参考如下博客,站在巨人的肩膀上。能够看得更远。