Java同步器之LockSupport源码分析
一、简介
LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport
中的park()
和unpark(thread)
的作用分别是阻塞线程和解除阻塞线程,而且park()
和unpark(thread)
不会遇到“Thread.suspend
和Thread.resume
所可能引发的死锁”问题。
调用
park()
的线程和另一个试图将其unpark(thread)
的线程之间的竞争将保持活性。
因为park()
和unpark()
有许可(permit
)的存在,permit
只有两个值1
和0
(默认)。permit
默认值是0
,所以一开始调用park()
方法,线程必定会被阻塞。调用unpark(thread)
方法后,会自动唤醒thread
线程,即park
方法立即返回。
- 当调用
unpark(thread)
方法,就会将thread
线程的许可permit
设置成1
(注意多次调用unpark
方法,不会累加,permit
值还是1
)。 - 当调用
park()
方法,如果当前线程的permit
是1
,那么将permit
设置为0
,并立即返回。如果当前线程的permit
是0
,那么当前线程就会阻塞,直到别的线程将当前线程的permit
设置为1
,park
方法会将permit
再次设置为0
,并返回。
二、案例
下面的“示例1”和“示例2”可以更清晰的了解LockSupport
的用法。
2.1 先park再unpark
import java.util.concurrent.locks.LockSupport;
public class LockSupportTest1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(LocalDateTime.now() + " start...");
//t1睡眠了一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(LocalDateTime.now() + " park...");
//t1线程一秒后暂停
LockSupport.park();
System.out.println(LocalDateTime.now() + " resume...");
}, "t1");
t1.start();
//主线程睡眠二秒
Thread.sleep(2000);
System.out.println(LocalDateTime.now() + " unpark...");
//二秒后由主线程恢复t1线程的运行
LockSupport.unpark(t1);
}
}
运行结果:
2023-02-08T16:58:30.425 start...
2023-02-08T16:58:31.430 park...
2023-02-08T16:58:32.385 unpark...
2023-02-08T16:58:32.386 resume...
分析:
t1
线程启动,休眠1
秒后,调用park
方法,暂停当前线程(也就是t1
线程暂停了),不在继续执行。当主线程休眠2
秒后,执行unpark
方法唤醒t1
线程,同时t1
线程被唤醒,继续执行。
原理:
先调用park
分析
- 当前线程调用
Unsafe.park()
方法; - 检查
_counter
,本情况为0
,这时,获得_mutex
互斥锁; - 线程进入
_cond
条件变量阻塞; - 设置
_counter=0
。
再调用unpark
分析
- 调用
Unsafe.unpark(Thread_0)
方法,设置_counter
为1
; - 唤醒
_cond
条件变量中的Thread_0
; Thread_0
恢复运行;- 设置
_counter
为0
。
2.2 先unpark再park
import java.util.concurrent.locks.LockSupport;
public class LockSupportTest2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(LocalDateTime.now() + " start...");
try {
//t1睡眠了两秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(LocalDateTime.now() + " park...");
//t1线程两秒后暂停
LockSupport.park();
System.out.println(LocalDateTime.now() + " resume...");
}, "t1");
t1.start();
//主线程睡眠一秒
Thread.sleep(1000);
System.out.println(LocalDateTime.now() + " unpark...");
//一秒后由主线程恢复t1线程的运行
LockSupport.unpark(t1);
}
}
运行结果 :
2023-02-08T16:59:57.452 start...
2023-02-08T16:59:58.423 unpark...
2023-02-08T16:59:59.457 park...
2023-02-08T16:59:59.457 resume...
分析:
t1
线程启动,休眠2
秒。主线程只休眠1
秒(此时t1
线程还在睡眠的过程中),主线程醒来后执行了unpark
方法,我们看到t1线程没有报错,也没有抛异常。当t1
线程睡够2
秒后执行park
方法,理应暂停线程,但是并没有停下来,几乎同时向下继续执行。
原理:
- 调用
Unsafe.unpark(Thread_0)
方法,设置_counter
为1
; - 当前线程调用
Unsafe.park()
方法; - 检查
_counter
,本情况为1
,这时线程无需阻塞,继续运行; - 设置
_counter
为0
。
三、源码
3.1 构造方法
LockSupport
类只提供了一个被private
修饰的构造方法,意味着LockSupport
不能在任何地方被实例化,但所有方法都是静态方法,可以在任意地方被调用。
/**
* 私有构造
*/
private LockSupport() {}
3.2 属性
/**
* 这个方法是由于多线程随机数生成器ThreadLocalRandom的package访问权限限制不能被这个包下的类使用,
* 复制了一份实现出来,在StampedLock中被使用
*/
static final int nextSecondarySeed() {
int r;
Thread t = Thread.currentThread();
if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
r ^= r << 13; // xorshift
r ^= r >>> 17;
r ^= r << 5;
}
else if ((r = java.util.concurrent.ThreadLocalRandom.current().nextInt()) == 0)
r = 1; // avoid zero
UNSAFE.putInt(t, SECONDARY, r);
return r;
}
/**
* /**
* * 三种情况停止阻塞:
* * 1. 调用unpark
* * 2. 线程被中断
* * 3. 设置时间到了
* * @Param isAbsolute 是否绝对时间
* * @Param time 时间 为0时代表无线等待
* *
* Unsafe.park(boolean isAbsolute,long time);
*
* /**
* * 释放某线程,需要保证释放时线程存活
* *
* Unsafe.unpark(Thread thread);
*/
private static final sun.misc.Unsafe UNSAFE;
/**
* 获取每个字段的偏移地址
*/
private static final long parkBlockerOffset;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
try {
/** 获取一个Unsafe实例 后续利用该实例获取偏移量 */
UNSAFE = sun.misc.Unsafe.getUnsafe();
/** 创建一个Thread的class对象 后续利用反射获取字段 */
Class<?> tk = Thread.class;
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) { throw new Error(ex); }
}
3.3 blocker
3.3.1 blocker的作用
在分析LockSupport
源码前,什么是blocker
,blocker
的作用是什么?
在Thread
类的源码中,可以找到这样一个被volatile
修饰的变量,LockSupport
类中所有blocker
的相关变量以及方法都是为这个parkBlocker
变量服务的。
volatile Object parkBlocker;
当线程被阻塞时,如果该线程的parkBlocker
变量不为空,则在打印堆栈异常时,控制台会打印输出具体阻塞对象的信息,方便错误排查。
3.3.2 blocker相关方法
首先关注blocker
相关方法:
/**
* 设置blocker
* 将线程t的parkBlocker字段的值更新为arg
*/
private static void setBlocker(Thread t, Object arg) {
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
/**
* 获取blocker
*/
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}
源码中可以清晰的看到方法中使用了parkBlockerOffset
,即在类初始化时获取的parkBlocker
在内存中的偏移量,putObject
和getObjectVolatile
方法采用地址加偏移量的方式从内存直接设置或获取parkBlocker
(Unsafe
包下的方法可以直接操作内存,因此该类被命名为Unsafe
),这样做的原因是因为线程被阻塞时无法被赋值或取值。
3.4 park
LockSupport
为阻塞操作提供了两组三类方法,一组是不设置blocker
的方法,另一组是设置blocker
方法的,JDK
推荐为Thread
设置blocker
方便调试。
/**
* 基础阻塞方法,无时限
*/
public static void park(Object blocker) {
/** 获取当前线程 */
Thread t = Thread.currentThread();
setBlocker(t, blocker);
/** 阻塞核心方法 直到被唤醒前不会执行下一语句 内部逻辑后文展开讨论 */
UNSAFE.park(false, 0L);
/** 线程被唤醒后parkBlocker重新置null */
setBlocker(t, null);
}
/**
* 逻辑与park基本相同
* 阻塞nanos纳秒 超时后线程被自动唤醒
*/
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, nanos);
setBlocker(t, null);
}
}
/**
* 逻辑与park基本相同
* 阻塞到日期deadline为止 超过deadline日期后自动被唤醒
*/
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
/**
* 无限等待其它线程将该线程unpark或中断该线程
*/
public static void park() {
UNSAFE.park(false, 0L);
}
/**
* 指定等待时间
*/
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
/**
* 阻塞到指定时刻
*/
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}
3.5 unpark
unpark
方法如下,UNSAFE.unpark
为唤醒线程的核心方法。
/**
* 释放许可,使线程继续运行。
* 如果线程没被阻塞,则下次park不会阻塞该线程?
*/
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
说明:LockSupport
是通过调用Unsafe
函数中的接口实现阻塞和解除阻塞的。
3.6 底层实现
在Unsafe
的源码中可见,park
与unpark
都是native
方法(意味着由C++
底层实现)。
void Parker::park(bool isAbsolute, jlong time) {
// 先原子的将_counter的值设为0,并返回_counter的原值,如果原值>0说明有通行证,直接返回
// 首先尝试能否获取许可
// 利用原子操作设_counter(许可)为0,同时返回_counter原值
// 原值为1时获取许可成功 直接return;
if (Atomic::xchg(0, &_counter) > 0) return;
Thread* thread = Thread::current();
assert(thread->is_Java_thread(), "Must be JavaThread");
JavaThread *jt = (JavaThread *)thread;
//如果线程被中断 直接返回
if (Thread::is_interrupted(thread, false)) {
return;
}
timespec absTime;
// 如果出现time小于0 或
// 调用了parkUntil方法(只用调用parkUntil时isAbsolute为true)且time为0的情况
// 意味着不需要尝试获取许可 直接返回
if (time < 0 || (isAbsolute && time == 0) ) {
return;
}
// 只有在java种调用了parkNanos或parkUntil方法才会进入此分支
if (time > 0) {
// 定时唤醒
unpackTime(&absTime, isAbsolute, time);
}
ThreadBlockInVM tbivm(jt);
// 如果线程被中断,直接返回
// 如果没有被中断,且获取互斥锁失败,直接返回
if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
return;
}
int status;
// 如果_counter > 0, 不需要等待,这里再次检查_counter的值
if (_counter > 0) {
_counter = 0;
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
// 插入写屏障
OrderAccess::fence();
return;
}
OSThreadWaitState osts(thread->osthread(), false);
// 暂停Java线程
jt->set_suspend_equivalent();
assert(_cur_index == -1, "invariant");
if (time == 0) {
_cur_index = REL_INDEX; // arbitrary choice when not timed
// 线程进入阻塞状态 并等待_cond[_cur_index]信号
status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
} else {
_cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
// 线程进入限时阻塞状态
status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ;
if (status != 0 && WorkAroundNPTLTimedWaitHang) {
pthread_cond_destroy (&_cond[_cur_index]) ;
pthread_cond_init (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr());
}
}
_cur_index = -1;
_counter = 0 ;
// 互斥锁释放
status = pthread_mutex_unlock(_mutex) ;
assert_status(status == 0, status, "invariant") ;
OrderAccess::fence();
}
void Parker::unpark() {
int s, status ;
status = pthread_mutex_lock(_mutex);
assert (status == 0, "invariant") ;
//保存原始许可值 用于后续判断
s = _counter;
//许可置1
_counter = 1;
if (s < 1) {
if (WorkAroundNPTLTimedWaitHang) {
//唤醒等待线程
status = pthread_cond_signal (_cond) ;
assert (status == 0, "invariant") ;
//释放锁操作
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
} else {
status = pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
status = pthread_cond_signal (_cond) ;
assert (status == 0, "invariant") ;
}
} else {
//释放锁
pthread_mutex_unlock(_mutex);
assert (status == 0, "invariant") ;
}
}
用mutex
和condition
保护了一个_counter
的变量,当park
时,这个变量置为了0
,当unpark
时,这个变量置为1
。
值得注意的是在park
函数里,调用pthread_cond_wait
时,并没有用while
来判断,所以posix condition
里的"Spurious wakeup"一样会传递到上层Java
的代码里。
四、总结
LockSupport
也是实现线程间通信的一种有效的方式。unpark
和park
之间不需要有严格的顺序。可以先执行unpark
,之后再执行park
。此外,LockSupport
全部是采用UnSafe
类来实现的。这个类通过使用park/unpark
以及相关cas
操作,就实现了java
中JUC
的各种复杂的数据结构和容器,而且效率非常高。
LockSupport
类使用了一种名为Permit
(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个permit
。permit
只有两个值1
和0
,默认是0
。- 可以把许可看成是一种(0,1)信号量(
Semaphore
),但与Semaphore
不同的是,许可的累加上限是1
。 permit
默认是0
,所以一开始调用park()
方法,当前线程就会阻塞,直到别的线程将当前线程的permit
设置为1
时,park()
方法会被唤醒,然后会将permit
再次设置为0
并返回。
五、拓展
5.1 park/unpark与wait/notify的区别
- 先唤醒再阻塞操作,由
LockSupport
实现,即先调用unpark
再调用park
,则该线程不会被阻塞;由Object
实现,即先调用notify
再调用wait
,则该线程会被阻塞。 LockSupport
允许在任意地方阻塞唤醒线程,Object
的wait/notify
必须在synchronized
同步代码块内调用。因为park/unpark
依赖许可量,wait/notify
依赖锁。LockSupport
允许唤醒指定线程,notify
只能唤醒随机线程,notifyAll
唤醒全部阻塞线程。
5.2 为什么可以先唤醒线程后阻塞线程
因为unpark
获取了一个凭证,之后再调用park
方法,就可以名正言顺的凭证消费,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1
,连续调用两次unpark
和调用一次unpark
效果一样,只会增加一个凭证;而调用两次park
却需要消费两个凭证,证不够,不能放行。