1.LockSupport 简介

  1. LockSupportJUC中提供的线程阻塞工具,它可以在线程内任意位置让线程阻塞。和Thread#suspend()相比,它弥补了由于Thread#resume()在前发生,导致线程无法继续执行的情况。和Object#wait()相比,它不需要先获得某个对象的锁,也不会抛出InterruptException异常。
  2. 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
2
3
4
5
6
7
8
9
10
11
12
13
public class Thread implements Runnable {
...

/**
* 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;

...
}

LockSoupport通过Unsafe对象的objectFieldOffset方法获取到parkBlocker在内存里的偏移量。

1
2
3
private static final Unsafe U = Unsafe.getUnsafe();
private static final long PARKBLOCKER = U.objectFieldOffset
(Thread.class, "parkBlocker");
1
2
3
4
5
// 给线程t设置对应的parkBlocker
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
U.putObject(t, PARKBLOCKER, arg);
}
1
2
3
4
5
6
// 获取当前线程的阻塞对象parkBlocker
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return U.getObjectVolatile(t, PARKBLOCKER);
}

问题 :为什么要通过指针(Unsafe)来获取parkBlocker,而不是通过Thread#getBlocker()获取?parkBlocker是在线程处于阻塞的情况下才会被赋值,如果不通过这种内存的方法,而是直接调用线程内的方法,线程是不会回应调用的。

3.2.park

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void park() {
U.park(false, 0L);
}
// false表示相对时间
public static void parkNanos(long nanos) {
if (nanos > 0)
U.park(false, nanos);
}

// true表示绝对时间
public static void parkUntil(long deadline) {
U.park(true, deadline);
}

下面我们来看可以指定阻塞对象的 park()

1
2
3
4
5
6
public 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
2
3
4
public static void unpark(Thread thread) {
if (thread != null)
U.unpark(thread);
}

4.特性

4.1.顺序无关

先调用unpark(),再调用park()时,仍能够正确实现同步,不会造成由wait()/notify()调用顺序不当所引起的阻塞,因此park()/unpark()相比wait()/notify()更加的灵活。

4.2.不可重入

如果一个线程连续2次调用park,那么该线程一定会一直阻塞下去。

1
2
3
4
5
6
7
8
9
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");
}

输出:
image.png
可以看到b3并没有输出,说明第二次调用park时线程被阻塞了。

4.3.响应中断

线程调用park()阻塞之后,如果该线程被中断,效果与unpark()一样,是不会抛出InterrupedException异常,只会默默的返回,我们可以通过Thread.interrupted()获得中断标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
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");
}

输出
image.png

4.4.面向线程

LockSupportpark()unpark()线程直接操作的就是线程,更符合语义,Objectwait()notify()它并不是直接对线程操作,它需要一个object来进行线程的挂起或唤醒。在调用对象的wait()之前当前线程必须先获得该对象的锁,被唤醒之后需要重新获得锁才能继续执行。虽然都能更改线程状态为WAITING,但由于实现机制的不同,所以不会产生交集,即park()挂起线程,notify()/notifyall()是无法进行唤醒的。

5.示例

5.1不可重入的公平锁的简单实现

这个是jdk文档中的一个示例,实现了一个先进先出的不可重入锁(互斥信号量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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);
// 如果当前线程不是等待线程队列第一个,或者锁已经被其他线程获取,则当前线程阻塞在FIFOMutex对象上
while (waiters.peek() != current ||
!locked.compareAndSet(false, true)) {
LockSupport.park(this);
// 如果在park过程中被中断,将中断标志位置为true,interrupted会清除中断标志位
if (Thread.interrupted())
wasInterrupted = true;
}
// 移除当前线程,表示成功获得锁
waiters.remove();
// 如果wasInterrupted为true,说明在获取锁的过程中被中断过,所以要中断当前线程。
// 维持正确的中断状态
if (wasInterrupted)
current.interrupt();
}

public void unlock() {
locked.set(false);
//唤醒当前线程队列中第一个元素
LockSupport.unpark(waiters.peek());
}
}

5.2通过jstack查看parkBlocker

1
2
3
4
public static void main(String[] args) {
Thread.currentThread().setName("my-thread");
LockSupport.park("my-blocker");
}

jps查看当前线程id
image.png

jstack 39678
image.png

可以看到线程my-thread处于WAITING状态,并且wait for 0x00000007957c01b0这个String对象。