LockSupport
概述
LockSupport是一个编程工具类,主要是为了阻塞和唤醒线程。它的所有方法都是静态方法,它可以让线程在任意位置阻塞,也可以在任意位置唤醒。
它可以在阻塞线程时为线程设置一个blocker,这个blocker是用来记录线程被阻塞时被谁阻塞的,用于线程监控和分析工具来定位原因。
LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("线程开始执行");
LockSupport.park();
System.out.println("线程执行结束");
});
thread.start();
TimeUnit.SECONDS.sleep(3);
System.out.println("执行unpark");
LockSupport.unpark(thread);
}
和wait/notify区别
- wait和notify都必须先获得锁对象才能调用,但是park不需要获取某个对象的锁就可以锁住线程。
- notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。
重要方法
这些方法都是调用Unsafe类的native方法
private static final sun.misc.Unsafe UNSAFE;
public final class Unsafe {
public native void park(boolean isAbsolute, long time);
public native void unpark(Thread jthread);
}
park(Object blocker)
setBlocker记录了当前线程是被blocker阻塞的,当线程在没有持有许可证的情况下调用park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的,所以推荐使用LockSupport.park(this);
如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被阻塞挂起。在其他线程调用unpark(Thread thread) 方法并且将当前线程作为参数时,调用park方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者被虚假唤醒,则阻塞线程也会返回。
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null); // 线程被激活后清除blocker变量
}
private static void setBlocker(Thread t, Object arg) {
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
unpark(Thread thread)
如果thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。
如果thread之前没有调用park,则让thread持有一个许可证,之后再调用park方法,则会立即返回。
public static void unpark(Thread thread) {
if (thread != null) UNSAFE.unpark(thread);
}
parkNanos(Object blocker, long nanos)
如果没有拿到许可证,则阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回
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);
}
}
parkUntil
阻塞当前线程,直到deadline;
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
原理浅析
只讲部分重点,非重点代码略去了
概述
每个java线程都有一个Parker实例,Parker类定义:
class Parker {
private:
volatile int _counter; // 记录许可
public:
void park(bool isAbsolute, jlong time);
void unpark();
}
LockSupport通过控制_counter进行线程的阻塞/唤醒,原理类似于信号量机制的PV操作,其中Semaphore初始为0,最多为1。
形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。
_counter
只能在0和1之间取值:当为1时,代表该类被unpark调用过,更多的调用,也不会增加_counter
的值,当该线程调用park()时,不会阻塞,同时_counter立刻清零。当为0时, 调用park()会被阻塞。
- 为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。 - 为什么唤醒两次后阻塞两次会阻塞线程。
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。
park()
- 检查
_counter
是否大于零(之前调用过unpark),则通过原子操作将_counter设置为0。线程不用阻塞并返回。 - 检查该线程是否有中断信号,如果有则清除该中断信号并返回(不抛出异常)。
- 尝试通过
pthread_mutex_trylock
对_mutex加锁来达到线程互斥。 - 检查park是否设置超时时间, 若设置了通过safe_cond_timedwait进行超时等待; 若没有设置,调用pthread_cond_wait进行阻塞等待。 这两个函数都在阻塞等待时都会放弃cpu的使用。 直到别的线程去唤醒它(调用pthread_cond_signal)。safe_cond_timedwait/pthread_cond_wait在执行之前肯定已经获取了锁_mutex, 在睡眠前释放了锁, 在被唤醒之前, 首先再去获取锁。
- 将_counter设置为零。
- 通过pthread_mutex_unlock释放锁。
void Parker::park(bool isAbsolute, jlong time) {
if (Atomic::xchg(0, &_counter) > 0) return; // 调用过unpark
if (Thread::is_interrupted(thread, false)) return; // 中断过
// 对_mutex加锁
if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
return;
}
// 进行超时等待或者阻塞等待,直到被signal唤醒
if (time == 0) {
status = pthread_cond_wait (&_cond[_cur_index], _mutex);
} else {
status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime);
}
_counter = 0; // 唤醒后消耗掉这个凭证
status = pthread_mutex_unlock(_mutex); // 解锁
}
unpark()
- 首先获取锁_mutex。
- 不管之前是什么值,都将_counter置为1,所以无论多少函数调用unpark(),都是无效的,只会记录一次。
- 检查线程是否已经被阻塞了,阻塞则调用pthread_cond_signal唤醒。
- 最后释放锁_mutex。
void Parker::unpark() {
status = pthread_mutex_lock(_mutex);
s = _counter;
_counter = 1; // 将_counter置1
if (s < 1) status = pthread_cond_signal (&_cond[_cur_index]); // 进行线程唤醒
pthread_mutex_unlock(_mutex);