LockSupport 详解
2024-09-30 08:00:49 # Technical # JavaConcurrency

为什么 LockSupport 是核心基础类?

写出分别通过 wait/notify 和 LockSupport 的 park/unpark 实现同步?

LockSupport.park() 会释放锁资源吗?那么 Condition.await() 呢?

Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park() 的区别?

如果在 wait() 之前执行了 notify() 会怎样?

如果在 park() 之前执行了 unpark() 会怎样?

LockSupport 用于提供阻塞和唤醒线程的功能。它允许线程在不使用传统的同步机制(如synchronized和ReentrantLock)的情况下被挂起和恢复,适用于实现更复杂的线程控制和等待机制

使用

利用 LockSupport 实现 N 个线程依次打印 1-100

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
private static volatile int count = 1;

private static final Thread[] threads = new Thread[10];

public static void main(String[] args) {
threads[0] = new Thread(() -> print(0));
threads[1] = new Thread(() -> print(1));
threads[2] = new Thread(() -> print(2));
threads[3] = new Thread(() -> print(3));
threads[4] = new Thread(() -> print(4));
threads[5] = new Thread(() -> print(5));
threads[6] = new Thread(() -> print(6));
threads[7] = new Thread(() -> print(7));
threads[8] = new Thread(() -> print(8));
threads[9] = new Thread(() -> print(9));
for (Thread thread : threads) {
thread.start();
}
}

private static void print(int threadId) {
int s = threads.length;
while (count <= 100) {
if (count % s != threadId) {
LockSupport.park();
// 唤醒后重新校验下条件
continue;
}
System.out.println("Thread-" + threadId + ": " + count);
count++;
LockSupport.unpark(threads[count % s]);
}
}

这里不加 continue 会导致?…

属性

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
public class LockSupport {
// Hotspot implementation via intrinsics API
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实例
UNSAFE = sun.misc.Unsafe.getUnsafe();
// 线程类类型
Class<?> tk = Thread.class;
// 获取Thread的parkBlocker字段的内存偏移地址
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
// 获取Thread的threadLocalRandomSeed字段的内存偏移地址
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
// 获取Thread的threadLocalRandomProbe字段的内存偏移地址
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
// 获取Thread的threadLocalRandomSecondarySeed字段的内存偏移地址
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) { throw new Error(ex); }
}
}

构造函数

1
private LockSupport() {}

LockSupport 只有一个私有的构造函数,说明其无法被实例化

核心方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 一直等待,直到获得许可
public static void park() {
UNSAFE.park(false, 0L);
}

// 等待最多 nanos 纳秒
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}

// 等待直到 deadline(时间戳)
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}

还有个指定 blocker 的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void park(Object blocker) {
// 获取当前线程
Thread t = Thread.currentThread();
// 设置 Blocker
setBlocker(t, blocker);
// 获取许可
UNSAFE.park(false, 0L);
// 重新可运行后再此设置 Blocker
setBlocker(t, null);
}

private static void setBlocker(Thread t, Object arg) {
// 设置线程 t 的 parkBlocker 字段的值为 arg
UNSAFE.putObject(t, parkBlockerOffset, arg);
}

这个 blocker 参数主要用于诊断目的:

  • 帮助识别为什么线程被阻塞
  • 在线程转储中提供更多信息
  • 便于调试复杂的并发问题

原理与优势

首先,需要理解两个关键概念:

  • 许可:可以把它想象成一种令牌,每个线程最多只有一个许可
  • 计数器:每个线程都有一个与之关联的计数器,用于记录许可的状态

park 方法原理

当调用 LockSupport.park()LockSupport.park(Object blocker) 时:

  • 首先检查许可的计数器,如果计数器大于 0,则立即返回,并将计数器 -1
  • 如果计数器为 0,则调用操作系统的方法将当前线程置为等待状态(WAITING/TIMED_WAITING)
  • 线程会一直等待,直到以下情况发生之一:a) 其他线程调用了 LockSupport.unpark(Thread) 方法,将该线程作为参数传入;b) 其他线程中断了该线程;c) 等待操作无缘故返回(这种情况被称为「虚假唤醒」,非常罕见)

unpark 方法原理

  • 如果指定线程的许可计数器为 0,则将其设置为 1
  • 如果指定线程当前因 park 操作而被阻塞,则唤醒该线程
  • 如果指定线程当前没有被阻塞,则什么也不做,只是保证下一次 park 操作不会阻塞

优势

与传统的 wait/notify 机制相比,有以下几个优势:

  • 不需要在同步块中使用
  • park 和 unpark 可以按任意顺序调用,即使 unpark 在 park 之前调用,后续的 park 也不会阻塞
  • 使用起来更加简单,不易出错,不会抛出 InterruptedException 异常

注意事项

  • park 方法不保证线程一定会阻塞。如果在调用 park 之前或者在 park 阻塞期间,有其他线程调用了 unpark,那么 park 可能会立即返回
  • 连续多次调用 unpark 只会累积一个许可