LockSupport源码分析
1.LockSupport 简介
LockSupport
是JUC
中提供的线程阻塞工具,它可以在线程内任意位置让线程阻塞。和Thread#suspend()
相比,它弥补了由于Thread#resume()
在前发生,导致线程无法继续执行的情况。和Object#wait()
相比,它不需要先获得某个对象的锁,也不会抛出InterruptException
异常。LockSupport
提供了park()
和unpark()
方法实现阻塞当前线程和唤醒当前阻塞的线程。LockSupport
为每个使用它的线程都提供了一个许可(permit)——类似于一个二元信号量(每个线程只有1个许可能使用)。如果许可可用,那么park()
会立即返回,并且消费这个许可(将这个许可变为不可用),如果许可不可用,那么park()
就会阻塞当前线程。而unpark()
则使得对应的线程的许可变得可用。正是通过这种方式就实现了线程的阻塞与唤醒,也正是这个特点使得即使unpark()
发生在park()
之前,仍然可以使park()
操作立即返回,而不是像suspend()
虽然当前线程的状态是RUNNING
,但是却无法继续执行。
2.方法摘要
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) |
唤醒处于阻塞状态的线程。 |
getBlocker(Thread t) |
获取阻塞线程t的阻塞对象。 |
3.源码分析
3.1.parkBlocker
Thread
类中有一个重要的属性parkBlocker
,就是上面说的线程对应的阻塞对象,它记录了当前线程阻塞在哪个对象上,当程序出现问题时候,通过线程监控分析工具可以找出问题所在。当线程被unpark()
唤醒后,这个属性就会设置为null
(具体代码见3.3节)。
1 | public class Thread implements Runnable { |
LockSoupport
通过Unsafe
对象的objectFieldOffset
方法获取到parkBlocker
在内存里的偏移量。
1 | private static final Unsafe U = Unsafe.getUnsafe(); |
1 | // 给线程t设置对应的parkBlocker |
1 | // 获取当前线程的阻塞对象parkBlocker |
问题 :为什么要通过指针(
Unsafe
)来获取parkBlocker
,而不是通过Thread#getBlocker()
获取?parkBlocker是在线程处于阻塞的情况下才会被赋值,如果不通过这种内存的方法,而是直接调用线程内的方法,线程是不会回应调用的。
3.2.park
1 | public static void park() { |
下面我们来看可以指定阻塞对象的 park()
:1
2
3
4
5
6public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.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。
3.3.unpark
1 | public static void unpark(Thread thread) { |
4.特性
4.1.顺序无关
先调用
unpark()
,再调用park()
时,仍能够正确实现同步,不会造成由wait()/notify()
调用顺序不当所引起的阻塞,因此park()/unpark()
相比wait()/notify()
更加的灵活。
4.2.不可重入
如果一个线程连续2次调用
park
,那么该线程一定会一直阻塞下去。
1 | public static void noReentrantTest(){ |
输出:
可以看到b3并没有输出,说明第二次调用park
时线程被阻塞了。
4.3.响应中断
线程调用
park()
阻塞之后,如果该线程被中断,效果与unpark()
一样,是不会抛出InterrupedException
异常,只会默默的返回,我们可以通过Thread.interrupted()
获得中断标记。
1 | public static void interruptTest() throws Exception{ |
输出:
4.4.面向线程
LockSupport
的park()
和unpark()
线程直接操作的就是线程,更符合语义,Object
的wait()
和notify()
它并不是直接对线程操作,它需要一个object
来进行线程的挂起或唤醒。在调用对象的wait()
之前当前线程必须先获得该对象的锁,被唤醒之后需要重新获得锁才能继续执行。虽然都能更改线程状态为WAITING
,但由于实现机制的不同,所以不会产生交集,即park()
挂起线程,notify()/notifyall()
是无法进行唤醒的。
5.示例
5.1不可重入的公平锁的简单实现
这个是jdk文档中的一个示例,实现了一个先进先出的不可重入锁(互斥信号量)。
1 | class FIFOMutex { |
5.2通过jstack查看parkBlocker
1 | public static void main(String[] args) { |
jps查看当前线程id
jstack 39678
可以看到线程
my-thread
处于WAITING
状态,并且wait for
0x00000007957c01b0这个String
对象。