LockSupport详解
# LockSupport详解
# 1、LockSupport简介
LockSupport 是 Java 并发包(java.util.concurrent)中的一个工具类,提供了基本的线程阻塞和唤醒机制。它主要用于实现锁和其他同步类,如AQS、 ReentrantLock、Semaphore、CountDownLatch 等。
LockSupport 提供的主要方法是 park
和 unpark
,它们分别用于挂起和唤醒线程(LockSupport.park()
会让线程进入WATING状态)。 AQS中对线程进行挂起和唤醒操作最终使用的就是LockSupport.park(xxx);
和LockSupport.unpark(xxx)
。 AQS相关内容可以参考 我的上一篇博客 AQS详解 (opens new window)。
# LockSupport 类的构造方法
// 构造方法是私有的,意味着LockSupport类不能被实例化。
// 这个设计表明LockSupport类只是一个工具类,提供静态方法供外部调用。
private LockSupport() {}
# LockSupport 类的属性
// 这个静态常量持有一个Unsafe类的实例。Unsafe类提供了一些底层操作的能力。
// 如直接内存访问、CAS操作等。由于其强大的功能和潜在的危险性,Unsafe类的使用受到严格限制,通常只在JDK内部使用。
private static final sun.misc.Unsafe UNSAFE;
//parkBlockerOffset常量保存了Thread类中parkBlocker字段的内存偏移量。parkBlocker字段用于记录调用park方法时的阻塞对象。
private static final long parkBlockerOffset;
// 这些静态常量分别保存了Thread类中
// threadLocalRandomSeed
// threadLocalRandomProbe
// threadLocalRandomSecondarySeed字段的内存偏移量。
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
try {
// 获取 Unsafe 实例
UNSAFE = sun.misc.Unsafe.getUnsafe();
// 获取 Thread 类的相关字段的偏移量
Class<?> tk = Thread.class;
// 获取 parkBlocker 字段的偏移量
parkBlockerOffset = UNSAFE.objectFieldOffset(tk.getDeclaredField("parkBlocker"));
// 获取 threadLocalRandomSeed 字段的偏移量
SEED = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSeed"));
// 获取 threadLocalRandomProbe 字段的偏移量
PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));
// 获取 threadLocalRandomSecondarySeed 字段的偏移量
SECONDARY = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) {
throw new Error(ex);
}
}
总结:
LockSupport类提供了线程阻塞和唤醒的基础设施,通过使用Unsafe类进行底层内存操作来实现。其核心是利用park和unpark方法来管理线程的状态,确保并发编程中的高效和安全。
通过获取Thread类中关键字段的偏移量,LockSupport类能够直接操作这些字段,实现对线程状态的控制。
关于sun.misc.Unsafe UNSAFE
类,后续再另写一篇博客分析。
# Thread
类的parkBlocker
属性
volatile Object parkBlocker;
Thread类的parkBlocker属性是一个volatile的Object类型变量,用于记录线程在调用LockSupport.park(Object blocker)时被阻塞的原因或对象。通过parkBlocker,我们可以在调试或分析时更容易地了解线程的阻塞原因。这种设计有助于提高并发编程的可调试性和可维护性。
比如有一个锁对象,线程在获取锁时被阻塞,可以通过parkBlocker记录这个锁对象,以便在调试或分析时知道线程因为什么原因被阻塞。
为啥这样设计呢?
因为在JDK1.5的时候 LockSupport 没设计Thread
类的parkBlocker来记录阻塞信息,这导致分析线程变得困难。所以1.6加入了这个特性。 此外虽然 synchronized 本身不支持像 parkBlocker 这样的灵活机制但是在 JVM 内部,synchronized 会记录阻塞线程的锁对象,这也有利于调试。
下面的park(Object blocker)
方法就是这么用的。
# LockSupport 类的常用方法
# 挂起线程的相关方法
①、park(Object blocker)
方法:
挂起当前线程,并设置阻塞对象。阻塞对象通常用于调试或监视线程状态。挂起结束后清除阻塞对象。
public static void park(Object blocker) {
// 获取当前线程
Thread t = Thread.currentThread();
// 设置阻塞对象
setBlocker(t, blocker);
// 挂起当前线程
UNSAFE.park(false, 0L);
// 注意: (这里因为上面的 UNSAFE.park(false, 0L) 会让线程挂起)
// 当线程被唤醒的时候必须要清除唤醒线程的blocker对象 或者下面直接跟了setBlocker(t, null);
// 这就保证了当线程被唤醒之后能够确保清除唤醒线程的blocker对象
// 挂起结束后清除阻塞对象
setBlocker(t, null);
}
// 通过 UNSAFE.putObject 方法将阻塞对象 arg 设置到线程 t 中的 parkBlocker 字段中。
private static void setBlocker(Thread t, Object arg) {
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
// park 方法是一个本地方法,具体实现依赖于底层操作系统
public native void park(boolean isAbsolute, long time);
②、parkNanos(Object blocker, long nanos)
方法:
挂起当前线程指定的纳秒数,并设置阻塞对象。挂起结束后清除阻塞对象。
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
// 获取当前线程
Thread t = Thread.currentThread();
// 设置阻塞对象
setBlocker(t, blocker);
// 挂起当前线程指定的纳秒数
UNSAFE.park(false, nanos);
// 挂起结束后清除阻塞对象
setBlocker(t, null);
}
}
③、parkUntil(Object blocker, long deadline)
方法:
挂起当前线程直到指定的时间,并设置阻塞对象。挂起结束后清除阻塞对象。
public static void parkUntil(Object blocker, long deadline) {
// 获取当前线程
Thread t = Thread.currentThread();
// 设置阻塞对象
setBlocker(t, blocker);
// 挂起当前线程直到指定的时间
UNSAFE.park(true, deadline);
// 挂起结束后清除阻塞对象
setBlocker(t, null);
}
④、park()
方法:
挂起当前线程,直到其他线程调用unpark或线程被中断。
public static void park() {
// 挂起当前线程
UNSAFE.park(false, 0L);
}
⑤、parkNanos(long nanos)
方法:
挂起当前线程指定的纳秒数。
public static void parkNanos(long nanos) {
if (nanos > 0) {
// 挂起当前线程指定的纳秒数
UNSAFE.park(false, nanos);
}
}
⑥、parkUntil(long deadline)
方法:
挂起当前线程直到指定的时间。
public static void parkUntil(long deadline) {
// 挂起当前线程直到指定的时间
UNSAFE.park(true, deadline);
}
# 唤醒线程的相关方法
unpark(Thread thread)
方法:
unpark
方法用于唤醒被park
方法挂起的线程。
public static void unpark(Thread thread) {
// 检查传入的线程是否为 null
if (thread != null) {
// 使用 UNSAFE 类的 unpark 方法唤醒指定的线程
UNSAFE.unpark(thread);
}
}
# unpark(Thread thread)
方法注意点
这个方法上注释有一句话: This operation is not guaranteed to have any effect at all if the given thread has not been started. 意思是:如果给定的线程尚未启动(也就是线程调用start方法之前的状态),则无法保证此操作有效。
比如对一个处于NEW
状态的线程 调用unpark
,再让线程start,当线程处于RUNNABLE
状态后再调用park
方法,那么这个线程可能会被挂起。也就是 unpark
方法不一定生效。
这里先留个印象,下面第2节 分析 LockSupport原理 还会提到。
看下面的例子:
import java.util.concurrent.locks.LockSupport;
public class TestA {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// ③、t1此时的状态是 RUNNABLE ,再 park t1线程
LockSupport.park(); // 可能会阻塞 因为 unpark方法不保证 对未启动的线程生效
System.out.println("123");
}, "t1");
// ①、先 unpark NEW状态的 线程 t1
LockSupport.unpark(t1);
// ②、启动 t1线程
t1.start();
}
}
上面代码, t1线程尚未启动(也就是线程调用start方法之前的状态) LockSupport.unpark(t1)
未生效。
# LockSupport使用示例
利用LockSupport,实现线程间通信,交替打印 牛 马 人
import java.util.concurrent.locks.LockSupport;
public class TestA {
// t1负责打印牛
// t2负责打印马
// t3负责打印人
private static Thread t1, t2, t3;
// 控制打印顺序的状态变量 0表示执行t1 唤醒t2
// 控制打印顺序的状态变量 1表示执行t2 唤醒t3
// 控制打印顺序的状态变量 2表示执行t3 唤醒t1
private static volatile int state = 0;
public static void main(String[] args) {
t1 = new Thread(() -> {
for (int i = 0; i < 3; ++i) {
while (state != 0) {
LockSupport.park(); // 挂起 t1 线程,直到被唤醒
}
System.out.print("牛 ");
state = 1; // 设置为 1,表示下一个打印马
LockSupport.unpark(t2); // 唤醒 t2
}
});
t2 = new Thread(() -> {
for (int i = 0; i < 3; ++i) {
while (state != 1) {
LockSupport.park(); // 挂起 t2 线程,直到被唤醒
}
System.out.print("马 ");
state = 2; // 设置为 2,表示下一个打印人
LockSupport.unpark(t3); // 唤醒 t3
}
});
t3 = new Thread(() -> {
for (int i = 0; i < 3; ++i) {
while (state != 2) {
LockSupport.park(); // 挂起 t3 线程,直到被唤醒
}
System.out.print("人 ");
state = 0; // 设置为 0,表示下一个打印牛
LockSupport.unpark(t1); // 唤醒 t1
}
});
t1.start();
t2.start();
t3.start();
}
}
运行结果:
牛 马 人 牛 马 人 牛 马 人
# 判断park
的条件建议使用while
而不是if
LockSupport源码中注释也是这么建议的:
while (!canProceed()) { ... LockSupport.park(this); }
为什么这么用呢?
如果使用 if 条件,线程被唤醒后只检查一次条件。如果是条件不满足的唤醒,但线程已经继续执行,这会导致错误的行为。
而while 循环在每次被唤醒时都会重新检查条件。如果条件仍然不满足,线程会继续等待。这确保了线程在条件未满足时不会继续执行。
比如上面打印牛马人示例中:
new Thread(() -> {
for (int i = 0; i < 3; ++i) {
while (state != 2) { // 这里的while 换成两次if也可以
LockSupport.park(); // 挂起 t3 线程,直到被唤醒
}
System.out.print("人 ");
state = 0; // 设置为 0,表示下一个打印牛
LockSupport.unpark(t1); // 唤醒 t1
}
});
// 上代码可以用下面代码替换 因为条件比较简单
new Thread(() -> {
for (int i = 0; i < 3; ++i) {
if (state != 2) {
LockSupport.park(); // 挂起 t3 线程,直到被唤醒
}
if (state == 2) {
System.out.print("人 ");
state = 0; // 设置为 0,表示下一个打印牛
LockSupport.unpark(t1); // 唤醒 t1
}else {
LockSupport.park();
}
}
});
明显可以看出,使用while更加简洁明了。 实际上即使是很简单的state != 2
的条件用if
看着也很臃肿了,如果条件比较复杂就更不推荐多个if检查条件了,会让代码变得臃肿且难以理解和维护。
# 引出Guarded Suspension模式
在《图解Java多线程设计模式》这本书中第82页就有介绍这个模式。下面引用一下原书的内容。
Guarded是被守护、被保卫、被保护的意思Suspension则是“暂停”的意思。如果执行现在的处理会造成问题,就让执行处理的线程进行等待--这就是 Guarded Suspension 模式。
当你正在家换衣服时,门铃突然响了,原来是邮递员来送邮件了。这时,因为正在换衣服出不去,所以只能先喊道“请稍等一下”,让邮递员在门口稍等一会儿。换好衣服后,才说着“让您久等了”并打开门。
这个例子很日式呀,让您久等了~ 因为这本书作者就是日本人 。 非常推荐大家看看这本书《图解Java多线程设计模式》 。
再拿上面打印牛马人的例子说明一下问题,其中利用 while
来判断park
条件 就是为了保证线程只打印该打印的东西,不该打印的时候就挂起。
# 总结
LockSupport类提供了多种挂起线程的方法,主要是通过Unsafe类的park方法实现。挂起线程的方法分为带阻塞对象的和不带阻塞对象的,带阻塞对象的方法在挂起线程时会记录阻塞的原因或对象,以便调试和监视。每种方法还支持不同的挂起时长,包括无限期挂起、挂起指定的纳秒数和挂起直到某个时间点。通过这些方法,LockSupport类为高级并发控制提供了基础设施。比如AQS以及依赖AQS为基础实现的锁或者同步器,最终都是通过LockSupport提供的park
和unpark
实现的线程挂起和唤醒。
# 2、LockSupport原理分析
先思考一个问题
看下面代码:
import java.util.concurrent.locks.LockSupport;
public class TestA {
public static void main(String[] args) throws InterruptedException {
LockSupport.unpark(Thread.currentThread());
LockSupport.park();
System.out.println("LockSupport 先unpark 后park");
TestA testA = new TestA();
synchronized (testA) {
testA.notify();
testA.wait();
System.out.println("synchronized 先notify 后wait");
}
}
}
运行结果:
根据代码执行的结果可以看出:
代码调用 LockSupport.unpark
来唤醒当前线程。即使当前线程没有被阻塞,这个操作也会起作用。
当前线程已经调用过一次 LockSupport.unpark
后,再调用LockSupport.park()
就不会被阻塞了,因为已经提前唤醒了一次。
而synchronized
同步代码块中 先调用 notify没有任何作用,因为没有线程在 testA锁对象 上等待。
当前线程已经调用过一次 testA.notify();
后,再调用testA.wait();
线程仍然会被阻塞。
总结:
LockSupport
提供了更灵活的阻塞和唤醒机制,能够在任何时候调用 unpark 而不必担心线程是否已经阻塞。
下面这段内容来自Java官方的JDK1.6中文版文档。 (如果英语掌握的很好,还是推荐自己去阅读源码上的英文注释比较好,虽然下面是官方的JDK1.6中文版文档,但是翻译的还是比较生硬~)
用来创建锁和其他同步类的基本线程阻塞原语。
此类以及每个使用它的线程与一个许可关联(从 Semaphore 类的意义上说)。如果该许可可用,并且可在进程中使用,则调用 park 将立即返
回;否则可能 阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。(但与 Semaphore 不同的是,许可不能累积,并且最多只能有一个
许可。)
park 和 unpark 方法提供了阻塞和解除阻塞线程的有效方法,并且不会遇到导致过时方法 Thread.suspend 和 Thread.resume 因为以
下目的变得不可用的问题:由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。此外,如果调用者线
程被中断,并且支持超时,则 park 将返回。park 方法还可以在其他任何时间“毫无理由”地返回,因此通常必须在重新检查返回条件的循环里
调用此方法。从这个意义上说,park 是“忙碌等待”的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高
效。
三种形式的 park 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。
(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。
在锁实现中提供的作为 blocker 的普通参数是 this。
这些方法被设计用来作为创建高级同步实用工具的工具,对于大多数并发控制应用程序而言,它们本身并不是很有用。park 方法仅设计用于以下
形式的构造:
while (!canProceed()) { ... LockSupport.park(this); }在这里,在调用 park 之前,canProceed 和其他任何动作都不会锁定
或阻塞。因为每个线程只与一个许可关联,park 的任何中间使用都可能干扰其预期效果。
示例用法。 以下是一个先进先出 (first-in-first-out) 非重入锁类的框架。
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);
// Block while not first in queue or cannot acquire lock
while (waiters.peek() != current ||
!locked.compareAndSet(false, true)) {
LockSupport.park(this);
if (Thread.interrupted()) // ignore interrupts while waiting
wasInterrupted = true;
}
waiters.remove();
if (wasInterrupted) // reassert interrupt status on exit
current.interrupt();
}
public void unlock() {
locked.set(false);
LockSupport.unpark(waiters.peek());
}
}
# 总结下LockSupport类设计思路的关键点:
①、许可机制: LockSupport通过一个许可的概念来控制线程的阻塞和恢复。这个许可类似于Semaphore中的许可,但需要注意的是它不支持累积,即最多只有一个许可可用。这意味着即使多次调用unpark也只会保留一个许可。
②、阻塞操作: LockSupport.park()方法会尝试获取许可。如果许可存在,线程会立即返回并继续执行。如果没有许可,线程将被阻塞,直到另一个线程调用LockSupport.unpark(Thread)方法来释放许可,或者线程接收到中断信号。
③、非累积性: 与Semaphore不同,LockSupport的许可不能累积。这意味着即使一个线程被多次unpark,它也只能被唤醒一次,多余的unpark操作不会产生额外的效果。 例如:
import java.util.concurrent.locks.LockSupport;
public class TestA {
public static void main(String[] args) {
// unpark 主线程两次
LockSupport.unpark(Thread.currentThread());
LockSupport.unpark(Thread.currentThread());
// park主线程两次
LockSupport.park();
System.out.println("park第一次"); // 正常打印
LockSupport.park(); // 在这就会阻塞
System.out.println("park第二次"); // 不打印
}
}
④线程关联: 每个线程都有一个与之关联的LockSupport许可。当一个线程调用unpark时,它必须指定要唤醒的线程。这允许了更细粒度的控制,因为可以精确地选择哪个线程应该被唤醒。
⑤、中断处理: 当一个线程被阻塞时,它可以被中断。LockSupport.park()会检查中断状态,并在检测到中断时抛出InterruptedException。这使得LockSupport可以安全地与其他Java中断机制协同工作。
至于再底层的实现就是通过Unsafe类,以及操作系统提供的原语了。
例如,在 Linux 上,LockSupport 可能会利用 pthread 库中的 pthread_mutex_lock 和 pthread_cond_wait 等函数来实现这些功能。这里就不再细说了,水平有限~
# 3、park\unpark
VS wait\notify\sleep\await\singnal
直接列个表格简单直观:
方法 | 描述 | 对比 |
---|---|---|
park/unpark | park 阻塞当前线程,直到被 unpark 唤醒。unpark 唤醒指定线程。 | 直接操作线程,不依赖锁或条件。 可以在非同步块中使用。 提供更细粒度的控制。 park 可以指定阻塞时间。可以先 unpark 再 park 由于 park 不依赖于锁或者其他条件,所以如果一个线程持有锁,调用了park ,那么该线程挂起后并不会释放锁 |
wait/notify | wait 使当前线程等待,直到被 notify 或 notifyAll 唤醒。 | 需要在synchronized同步代码块中使用。wait 会释放持有的对象锁。notify 唤醒单个等待线程,notifyAll 唤醒所有等待线程。notify 在wait 之前调用没有效果 |
sleep | 使当前线程休眠指定时间。 | 不释放锁。 主要用于时间控制。 休眠时间结束后自动恢复。 |
await/signal | 用于 Condition 对象上的线程等待和唤醒。 | 必须在锁条件下使用。await 会释放锁并进入等待状态,直到被 signal 或 signalAll 唤醒。用于更复杂的同步需求。 |