LockSupport 工具类使用以及实现原理

JDK中的rt.jar包里面的是个LockSupport是个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。

LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。LockSupport是使用Unsafe类实现的,下面介绍LockSupport中的几个主要函数。

void park()

如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。

如下代码直接在main函数里面调用park方法,最终只会输出begin park!,然后当前线程被挂起,这是因为在默认情况下调用线程是不持有许可证的。

1
2
3
4
5
6
7
public static void main(String[] args) {
System.out.println("begin park! ");

LockSupport.park();

System.out.println("end park! ");
}

在其他线程调用unpark(Thread thread) 方法并且将当前线程作为参数时,调用park方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者被虚假唤醒,则阻塞线程也会返回。所以调用park方法时最好也使用循环条件判断方式。

需要注意的是,因调用park()方法而被阻塞的线程被其他线程中断而返回时并不会抛出InterruptedException异常。

void unpark(Thread thread)方法

当一个线程调用unpark时,如果参数thread线程没有持有thread与LockSupport类关联的许可证,则让thread线程持有。如果thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。如果thread之前没有调用park,则调用unpark方法后,再调用park方法,则会立即返回。修改代码如下。

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
System.out.println("begin park! ");

// 使用unpark使当前线程获取到许可证
LockSupport.unpark(Thread.currentThread());

// 再次调用park方法
LockSupport.park();

System.out.println("end park! ");
}

输出结果:

image-20200418210217993.png

下面再来看一个例子以加深对park和unpark的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("child thread begin park!");

// 调用park方法,挂起自己
LockSupport.park();

System.out.println("child thread unpark!");
}
});

thread.start();

Thread.sleep(1000);

System.out.println("main thread begin unpark!");

// 调用unpark方法让thread线程持有许可证,然后park方法返回
LockSupport.unpark(thread);
}

输出结果为

image-20200418231547193.png

上边代码执行过程如下:

  1. 首先创建了一个子线程thread,然后子线程启动调用park方法,由于默认情况下子线程没有持有许可证,因而会把自己挂起。
  2. 主线程休眠1s是为了让主线程调用unpark方法前让子线程输出child thread begin park!并阻塞
  3. 主线程执行unpark方法,参数为创建的子线程thread,这样做的目的是让子线程持有许可证,然后子线程调用park方法就返回了。

park方法返回时不会告诉你因何种原因返回,所以调用者需要根据之前调用park方法的原因,再次检查条件是否满足,如果不满足还需再次调用park方法。

例如,根据调用前后中断状态的对比就可以判断是不是因为被中断才返回的。

为了说明调用park方法后的线程被中断后会返回,我们修改上面的例子代码,删除LockSupport.unpark(thread),然后添加thread.interrupt(),具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("child thread begin park!");

while (!Thread.currentThread().isInterrupted()){
// 调用park方法,挂起自己,只有被中断才ui退出循环
LockSupport.park();
}

System.out.println("child thread unpark!");
}
});

thread.start();

Thread.sleep(1000);

System.out.println("main thread begin unpark!");

// 中断子线程
thread.interrupt();
}

输出结果:

image-20200418232751361.png

在如上代码中,只有中断子线程,子线程才会运行结束,如果子线程不被中断,即使调用unpark(thread)方法子线程也不会结束。

void parkNanos(long nanos)方法

和park方法类似,如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.parkNanos(Long nanos)方法后会马上返回。该方法的不同在于,如果没有拿到许可证,则调用线程会被挂起nanos时间后修改为自动返回。

void park(Object blocker)

park方法还支持带有blocker参数的方法 void park(Object blocker)方法,当线程在没有持有许可证的情况下调用park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。

使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的,所以JDK推荐我们使用带有blocker参数的park方法,并且blocker被设置为this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。

例如下面的代码。

1
2
3
4
5
6
7
8
9
10
11
public class TestPark {

public void testPark(){
LockSupport.park(); // (1)
}

public static void main(String[] args) {
TestPark testPark = new TestPark();
testPark.testPark();
}
}

运行代码后,使用jps查看运行进程号,然后通过jstack命令查看线程堆栈时可以啊看到如下输出结果。

image-20200419015628163.png

修改代码(1)为LockSupport.park(this) 后运行代码,则使用jstack命令输出结果为:image-20200419015832669.png

使用带blocker参数的park方法,线程堆栈可以提供更多有关阻塞对象的信息。

接下来看看内部实现

1
2
3
4
5
6
7
8
9
10
public static void park(Object blocker) {
// 获取调用线程
Thread t = Thread.currentThread();
// 设置线程的blocker变量
setBlocker(t, blocker);
// 挂起线程
UNSAFE.park(false, 0L);
// 线程被激活后清除blocker变量,因为一般都是在线程被阻塞时才分析原因
setBlocker(t, null);
}

Thread类里面有个变量volatile Object parkBlocker,用来存放park方法传递的blocker对象,也就是把blocker变量存放到了调用park方法的线程的成员变量里面。

void parkNanos(Object blocker, long nanos)

相比于park(Object blocker)方法多了个超时时间。

void parkUtil(Object blocker, long deadline)

它的代码如下:

1
2
3
4
5
6
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}

其中参数deadline的时间单位为ms,改时间是从1970年到现在某一时间点的毫秒值。这个方法和park(Object blocker, long nanos)方法的区别是,后则会是从当前算等待nonos秒时间,而前者是指定一个时间点,比如需要等到2019.11.11日11:11:11,则把则会个时间点转化为从1970年到这个时间点的总毫秒数。

最后再看一个例子。

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
public class FIFOMutex {
private final AtomicBoolean locked = new AtomicBoolean((false));
private final Queue<Thread> waiters = new ConcurrentLinkedDeque<Thread>();

public void lock(){
boolean wasInterrupted = false;
Thread current = Thread.currentThread();
waiters.add(current);

// 只有队首的线程可以获取锁(1)
while (waiters.peek() != current || !locked.compareAndSet(false, true)){
LockSupport.park(this);
if (Thread.interrupted()){ // (2)
wasInterrupted = true;
}
}

waiters.remove();
if (wasInterrupted){ // (3)
current.interrupt();
}
}

public void unlock(){
locked.set(false);
LockSupport.unpark(waiters.peek());
}

}

这是一个先进先出的锁,也就是只有队列的首元素可以获取锁。在代码(1)处,如果当前线程不是队首或者当前锁已经被其他线程获取,则调用park方法挂起自己。

然后在代码(2)处判断,如果park方法是因为被中断而返回,则忽略中断,并且重置中断标志,做个标记,然后再次判断当前线程是不是队首元素或者当前锁是否已经被其他线程获取,如果是则继续调用park方法挂起自己。

然后再代码(3)中,判断标记,如果标记为true则终端该标志,这个怎么理解呢,其实就是其他线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其他线程对该标志不感兴趣,所以要回恢复下。

喜欢关注公众号:

qrcode

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.