park与unpark基本使用与原理分析

一、概念

LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。

park,unpark这两个方法都是LockSupport类名下的方法,park用来暂停线程,unpark用来将暂停的线程恢复。

先park再unpark的方式是容易理解的。但还有一个场景,先unpark后再次执行park方法,也不会阻塞调用了park方法的线程

理解为park方法就是校验获取一个通行令牌,而unpark方法是获取到一个通行令牌的过程。先执行unpark方法,代表先获得了通行令牌。那么在另一个线程调用park方法时,校验到这个令牌存在,消耗掉这个令牌然后就可以继续往下走。

二、LockSupport函数列表

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
public class LockSupport {

// 返回提供给最近一次尚未解除阻塞的 park 方法调用的 blocker 对象,如果该调用不受阻塞,则返回 null。
static Object getBlocker(Thread t);

// 为了线程调度,禁用当前线程,除非许可可用。
static void park();

// 为了线程调度,在许可可用之前禁用当前线程。
static void park(Object blocker);

// 为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用。
static void parkNanos(long nanos);

// 为了线程调度,在许可可用前禁用当前线程,并最多等待指定的等待时间。
static void parkNanos(Object blocker, long nanos);

// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
static void parkUntil(long deadline);

// 为了线程调度,在指定的时限前禁用当前线程,除非许可可用。
static void parkUntil(Object blocker, long deadline);

// 如果给定线程的许可尚不可用,则使其可用。
static void unpark(Thread thread);
}

说明:LockSupport是通过调用Unsafe函数中的接口实现阻塞和解除阻塞的。

park()与park(Object blocker)区别说明

其他对应方法同样的原理,故以park()与park(Object blocker)为例进行说明

这两个方法都是用于线程调度的,但是有所不同:

  1. park() 方法会将当前线程挂起(暂停),直到被唤醒或者中断。
  2. park(Object blocker) 方法与 park() 方法类似,但是它还可以指定一个“阻塞器”对象,以便更好地进行调试和分析。当线程被阻塞时,阻塞器对象会被记录下来,方便程序员进行问题定位。

下面是使用代码举例:

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
// 示例 1: park()
Thread thread1 = new Thread(() -> {
System.out.println("线程开始运行");
LockSupport.park(); // 线程被挂起
System.out.println("线程被唤醒");
});
thread1.start();

Thread.sleep(3000);
LockSupport.unpark(thread1); // 唤醒线程

// 示例 2: park(Object blocker)
Object blocker = new Object();
Thread thread2 = new Thread(() -> {
System.out.println("线程开始运行");
LockSupport.park(blocker); // 线程被挂起,并记录阻塞器对象
System.out.println("线程被唤醒");
});
thread2.start();

Thread.sleep(3000);

System.out.println(LockSupport.getBlocker(thread2)); //输出thread2的地址值

LockSupport.unpark(thread2); // 唤醒线程

// 打印出阻塞器对象
System.out.println(LockSupport.getBlocker(thread2)); // 输出 null

输出结果:

1
2
3
4
5
6
线程开始运行
线程被唤醒
线程开始运行
java.lang.Object@4dd8dc3
线程被唤醒
null

三、基本使用

  • 先 park 再 unpark
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
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/10/22
* Description 先 park 再 unpark
*/
@Slf4j(topic = "c.Test01")
public class Test01 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}

输出结果

1
2
3
4
21:16:35.017 c.Test01 [t1] - start...
21:16:36.019 c.Test01 [t1] - park...
21:16:37.017 c.Test01 [main] - unpark...
21:16:37.017 c.Test01 [t1] - resume...
  • 先 unpark 再 park
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
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* Created by xiaowei
* Date 2022/10/22
* Description 先 unpark 再 park
*/
@Slf4j(topic = "c.Test02")
public class Test02 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}

输出结果

1
2
3
4
21:18:03.732 c.Test02 [t1] - start...
21:18:04.731 c.Test02 [main] - unpark...
21:18:05.735 c.Test02 [t1] - park...
21:18:05.735 c.Test02 [t1] - resume...

与Object的wait-notify对比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必;
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】;
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify。

中断响应

使用interrupt方法去打断唤醒线程,注意打断 park 线程, 不会清空打断状态,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws InterruptedException {
test3();
}

private static void test3() {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();


sleep(0.5);
t1.interrupt();
}

结果:

1
2
3
10:47:53.023 c.TestInterrupt [t1] - park...
10:47:53.535 c.TestInterrupt [t1] - unpark...
10:47:53.535 c.TestInterrupt [t1] - 打断状态:true

如果打断标记已经是 true, 则 park 会失效

1
2
3
4
5
6
7
8
9
10
11
12
private static void test4() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}
});
t1.start();
sleep(1);
t1.interrupt();
}

输出

1
2
3
4
5
6
7
8
9
10
21:13:48.783 [Thread-0] c.TestInterrupt - park... 
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.812 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true
21:13:49.813 [Thread-0] c.TestInterrupt - park...
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true

提示

可以使用 Thread.interrupted() 清除打断状态

四、原理

每个线程都会关联一个 Parker 对象,每个 Parker 对象都各自维护了三个角色:_counter(计数器)、 _mutex(互斥量)、_cond(条件变量)。

4.1 情况一:先调用park,再调用unpark

park 操作

  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量阻塞
  4. 设置 _counter = 0
unpark 操作

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 唤醒 _cond 条件变量中的 Thread_0
  3. Thread_0 恢复运行
  4. 设置 _counter 为 0

4.2 情况二:先调用unpark,再调用park

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0

park与unpark基本使用与原理分析
http://example.com/2023/03/28/park与unpark基本使用与原理分析/
作者
程序员小魏
发布于
2023年3月28日
许可协议