LockSupport源码分析
LockSupport 简介
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联-----类似于一个二元信号量(只有1个许可证可供使用)
方法摘要
methed | description |
---|---|
park() | 在许可可用之前阻塞当前线程。 |
park(Object blocker) | 在许可可用之前禁用当前线程,并指定阻塞对象。 |
parkNanos(long nanos) | 阻塞当前线程,最长不超过nanos纳秒。 |
parkNanos(Object blocker, long nanos) | 为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用,同时指定阻塞对象。 |
parkUntil(long deadline) | 阻塞当前线程直到deadline时间(绝对时间)。 |
parkUntil(Object blocker, long deadline) | 阻塞当前线程直到deadline时间,同时阻塞前记录当前线程等待对象的操作。 |
unpark(Thread thread) | 唤醒处于阻塞状态的线程Thread。 |
getBlocker(Thread t) | 获取阻塞线程t的Blocker对象。 |
源码分析
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 = sun.misc.Unsafe.getUnsafe();
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); }
}
先是通过反射机制获取
Thread
类的parkBlocker
字段对象。然后通过sun.misc.Unsafe
对象的objectFieldOffset
方法获取到parkBlocker
在内存里的偏移量。
/**
* The argument supplied to the current call to
* java.util.concurrent.locks.LockSupport.park.
* Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
* Accessed using java.util.concurrent.locks.LockSupport.getBlocker
*/
volatile Object parkBlocker;
这个字段是Thread中的一个字段,可以理解为专门为LockSupport而设计的,它被用来记录线程是被谁堵塞的,当程序出现问题时候,通过线程监控分析工具可以找出问题所在。
为什么要用偏移量来获取对象,而不是通过Thread#getBlocker()
获取?
parkBlocker是在线程处于阻塞的情况下才会被赋值。线程都已经阻塞了,如果不通过这种内存的方法,而是直接调用线程内的方法,线程是不会回应调用的。
parkNanos(long nanos)
与parkUntil(long deadline)
的区别?
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
由上面源码可知,true 表示绝对时间,false表示相对时间
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
setBlocker
为什么要调用两次?
第一次调用
setBlocker
,为当前线程的parkBlocker
设置blocker对象,然后调用park将当前线程挂起,此时当前线程已经阻塞了,等待unpark
被调用,所以第二个setBlocker
不能运行,当unpark
被调用之后,该线程获得许可,可以继续运行,此时把parkBlocker
设置为null
。如果不调用第二个setBlocker
,那么调用park
之后,直接调用getBlocker
函数,得到的还是上一次调用park(Object blocker)
设置的blocker,所以需要必须要保证在park(Object blocker)
整个函数执行完后,该线程的parkBlocker字段又恢复为null。
示例
官方文档的示例
一个简易的先进先出非重入锁(互斥信号量)的实现
class FIFOMutex {
//锁状态标识
private final AtomicBoolean locked = new AtomicBoolean(false);
//记录等待线程队列
private final Queue<Thread> waiters
= new ConcurrentLinkedQueue<Thread>();
public void lock() {
boolean wasInterrupted = false;
Thread current = Thread.currentThread();
waiters.add(current);
//如果当前线程不是等待线程队列第一个,或者locked状态已经是true,那么当前线程就要等待
while (waiters.peek() != current ||
!locked.compareAndSet(false, true)) {
LockSupport.park(this);
// 等待线程的中断线程标志位为true,就设置wasInterrupted为true,表示当前线程被中断
if (Thread.interrupted())
wasInterrupted = true;
}
//移除第一个元素。当前线程就是第一个元素,表示获得许可
waiters.remove();
//如果wasInterrupted为true,当前线程发出中断请求
if (wasInterrupted)
current.interrupt();
}
public void unlock() {
locked.set(false);
//唤醒当前线程队列中第一个元素
LockSupport.unpark(waiters.peek());
}
}
jstack查看blocker
public static void main(String[] args) {
Thread.currentThread().setName("my-thread");
LockSupport.park("my-blocker");
}
jps查看当前线程id
jstack 39678
可以看到线程
my-thread
处于WAITING
状态,并且wait for
0x00000007957c01b0这个String
对象
特性
- 顺序无关
先调用park,再调用unpark
public static void sequenceTest(){
Thread thread = Thread.currentThread();
LockSupport.park(thread);
System.out.println("b1");
LockSupport.park();
System.out.println("b2");
}
输出
先调用unpark,再调用park
todo 打算使用传统的生产者消费者模式来举例子
输出
在先调用unpark,再调用park时,仍能够正确实现同步
- 不可重入
public static void noReentrantTest(){
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);
System.out.println("b1");
LockSupport.park();
System.out.println("b2");
LockSupport.park();
System.out.println("b3");
}
输出:
可以看到b3并没有输出,说明第二次调用park
时线程被阻塞了。
- 响应中断
public static void interruptTest() throws Exception{
Thread thread = new Thread(() -> {
System.out.println("park begin");
LockSupport.park();
System.out.println("park end");
});
thread.start();
Thread.sleep(2000);
System.out.println("main interrupt");
thread.interrupt();
System.out.println("main end");
}
输出:
- 面向线程
总结
1.park
方法的调用一般要方法一个循环判断体里面。
如上述示例中的:
while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
LockSupport.park(this);
}
之所以这样做,是为了防止线程被唤醒后不进行判断而意外继续向下执行,这其实是一种Guarded Suspension
的多线程设计模式。
2.特性:
-
顺序无关:先调用
unpark
,再调用park
时,仍能够正确实现同步,不会造成由wait/notify
调用顺序不当所引起的阻塞,因此park/unpark
相比wait/notify
更加的灵活。 -
不可重入:如果一个线程连续2次调用
park
,那么该线程一定会一直阻塞下去。 -
响应中断:线程调用
park
阻塞之后,如果该线程被中断,此时interrupt
起到的作用与unpark
一样。 -
面向线程:
LockSupport
的park
和unpark
线程直接操作的就是线程,更符合语义,Object
的wait
和notify
它并不是直接对线程操作,它是被动的方法,它需要一个object
来进行线程的挂起或唤醒。在调用对象的wait
之前当前线程必须先获得该对象的锁,被唤醒之后需要重新获取到监视器才能继续执行。虽然都能更改线程状态为WAITING
,但由于实现的机制不同,所以不会产生交集,就是park
挂起线程,notify/notifyall
是无法进行唤醒的。