LockSupport核心原理分析
在前面分析AQS的时候,经常出现LockSupport.park(this);
,LockSupport.unpark(node.thread);
这类让线程等待唤醒状态的代码段,这类逻辑与线程的wait/notify功能类似,都是使当前线程等待/唤醒,但使用起来确实要比wait/notify方便的多。
LockSupport
本身很简单,无非就是对线程进行等待唤醒,所以下面从一个简单的demo入手,进行分析一下。
public static void main(String[] args) {
// 定义一个线程C,不干任何事情
Thread c = new Thread(()->{},"线程C");
// 定义一个线程B,执行一段输出后,自身进行等待
Thread b = new Thread(()->{
// 直接对线程C进行唤醒
LockSupport.unpark(c);
System.out.println(Thread.currentThread().getName() + ": 线程执行");
System.out.println(Thread.currentThread().getName() + ": 阻塞");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + ": 继续线程执行");
},"线程B");
// 定义一个线程C,自身等待一定时间后,对线程B进行唤醒
Thread a =new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + ": 线程执行");
LockSupport.parkNanos(1*3000*3000*3000);
System.out.println(Thread.currentThread().getName() + ": 线程继续执行");
// 对线程B进行唤醒
LockSupport.unpark(b);
} catch (Exception e) {}
},"线程A");
c.start();
b.start();
a.start();
}
看下输出效果
线程B: 线程执行
线程B: 阻塞
线程A: 线程执行
线程A: 线程继续执行
线程B: 继续线程执行
从代码及输出效果来看,这里明显跟wait/notify有较明显的区别(优势)
1)不需要在同步代码块中即可执行park,unpark逻辑
2)park与unpark不用成对出现
整体扫一下方法:
方法 | 描叙 |
---|---|
void park() | 阻塞当前线程,调用unpark可以从中断返回 |
void park(Object blocker) | 阻塞当前线程,但是会记录是导致阻塞的对象 |
void parkNanos(long nanos) | 阻塞当前线程nanos纳秒,时间过后会返回 |
void parkNanos(Object blocker, long nanos) | 阻塞当前线程nanos纳秒,但是会记录是导致阻塞的对象 |
void parkUntil(long deadline) | 阻塞当前线程直到deadline |
void parkUntil(Object blocker, long deadline) | 阻塞当前线程直到deadline,但是会记录是导致阻塞的对象 |
void unpark(Thread thread) | 唤醒指定的线程 |
以上方法都很简单明了,结合上面的demo,很容易理解。
那么LockSupport
具体是如何做到线程的阻塞与唤醒的呢,我们接下来分析一下。
void park(Object blocker)
// 阻塞线程逻辑
public static void park(Object blocker) {
// 获取当前线程
Thread t = Thread.currentThread();
// 设置阻塞对象
setBlocker(t, blocker);
// 阻塞线程:这里是线程直接阻塞的位置
UNSAFE.park(false, 0L);
// 线程被唤醒时,第一步就是将blocker设置为null
setBlocker(t, null);
}
// 设置阻塞对象
private static void setBlocker(Thread t, Object arg) {
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
...
// 对象内存偏移量,这个在直接设置对象内存时存储属性有用
private static final long parkBlockerOffset;
static {
try {
// 获取Unsafe对象(众所周知,这是一个”本地类“)
UNSAFE = sun.misc.Unsafe.getUnsafe();
// 获取线程的Class对象
Class<?> tk = Thread.class;
// 获取线程class中parkBlocker内存偏移量
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
...
} catch (Exception ex) { throw new Error(ex); }
}
...
在忽略Unsafe本地是如何阻塞的前提下,从上面代码整体上分析就是如下逻辑:
1)获取当前线程
2)设置阻塞当前线程的对象(这会很方便后续的问题排查,哪怕你显式打个日志都行)
3)调用UNSAFE.park进行阻塞
4)如果被唤醒,就清空阻塞当前线程的对象
至于unsafe本地方法如何注册,如何被寻址调用,这个在《认识UnSafe》
一章中有详细分析,所以这里就不再降叙其过程
这里贴一下unsafe.cpp中的代码逻辑(至于怎么找到unsafe.cpp,可以回去看《认识UnSafe》
这一节内容)
UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time))
UnsafeWrapper("Unsafe_Park");
EventThreadPark event;
#ifndef USDT2
HS_DTRACE_PROBE3(hotspot, thread__park__begin, thread->parker(), (int) isAbsolute, time);
#else /* USDT2 */
HOTSPOT_THREAD_PARK_BEGIN(
(uintptr_t) thread->parker(), (int) isAbsolute, time);
#endif /* USDT2 */
JavaThreadParkedState jtps(thread, time != 0);
// 核心逻辑
thread->parker()->park(isAbsolute != 0, time);
#ifndef USDT2
HS_DTRACE_PROBE1(hotspot, thread__park__end, thread->parker());
#else /* USDT2 */
HOTSPOT_THREAD_PARK_END(
(uintptr_t) thread->parker());
#endif /* USDT2 */
if (event.should_commit()) {
oop obj = thread->current_park_blocker();
event.set_klass((obj != NULL) ? obj->klass() : NULL);
event.set_timeout(time);
event.set_address((obj != NULL) ? (TYPE_ADDRESS) cast_from_oop<uintptr_t>(obj) : 0);
event.commit();
}
UNSAFE_END
看到最中间那行代码:
thread->parker()->park(isAbsolute != 0, time);
这里再强调一点就是,为什么java是跨平台的,就是因为对于本地方法在不同的操作系统上有不同的实现机制
,因此才有了跨平台
一说,这里可以去具体的操作系统上(比如Linux)去找到具体的实现逻辑,就能发现是如何阻塞线程了。这里就不去贴Linux上的代码实现了。
void unpark(Thread thread)
分析完void park(Object blocker)
逻辑,在分析unpark那就简要过一下就好了
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
直接将thread作为变量,调用unsafe逻辑进行唤醒
看看unsafe.cpp的代码
UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread))
UnsafeWrapper("Unsafe_Unpark");
Parker* p = NULL;
if (jthread != NULL) {
oop java_thread = JNIHandles::resolve_non_null(jthread);
if (java_thread != NULL) {
jlong lp = java_lang_Thread::park_event(java_thread);
if (lp != 0) {
// This cast is OK even though the jlong might have been read
// non-atomically on 32bit systems, since there, one word will
// always be zero anyway and the value set is always the same
p = (Parker*)addr_from_java(lp);
} else {
// Grab lock if apparently null or using older version of library
MutexLocker mu(Threads_lock);
java_thread = JNIHandles::resolve_non_null(jthread);
if (java_thread != NULL) {
JavaThread* thr = java_lang_Thread::thread(java_thread);
if (thr != NULL) {
p = thr->parker();
if (p != NULL) { // Bind to Java thread for next time.
java_lang_Thread::set_park_event(java_thread, addr_to_java(p));
}
}
}
}
}
}
if (p != NULL) {
#ifndef USDT2
HS_DTRACE_PROBE1(hotspot, thread__unpark, p);
#else /* USDT2 */
HOTSPOT_THREAD_UNPARK(
(uintptr_t) p);
#endif /* USDT2 */
p->unpark();
}
UNSAFE_END
看到p->unpark();
逻辑就知道去对应的操作系统上调用具体逻辑了,这里也不往下分析了。
总结
整体上<u>LockSupport</u>分析起来是最简单的,整个上层没什么复杂逻辑,无非就是阻塞唤醒(支持阻塞对象的记录,对了,这一点在是在jdk6加上的,jdk5是没有的,为了与wait/notify保持一致吧,为啥wait/notify有呢?因为传统的synchronized一般是针对对象锁的,因此会记录当前的对象信息的)
具体要想继续深入,那就去看看unsafe的整个调用链路及各个操作系统对于park/unpark的具体逻辑就好。
网友评论