LockSupport


LockSupport 简介

并发锁LockSupport原理剖析,四千字多图讲解+多例子+代码分析

LockSupport详解

LockSupport:灵活的线程工具类


LockSupport简介

LockSupport 是 Java 并发包(java.util.concurrent)中的一个工具类,是 Java 并发编程中一个非常重要的组件,提供了基本的线程阻塞和唤醒机制。我们熟知的并发组件 Lock、线程池、CountDownLatch 等都是基于 AQS 实现的,而 AQS 内部控制线程阻塞和唤醒又是通过 LockSupport 来实现的。

从该类的注释上也可以发现,它是一个控制线程阻塞和唤醒的工具,与以往的不同是它解决了曾经 wait()、notify()、await()、signal() 的局限。

LockSupport 提供的主要方法是 parkunpark,它们分别用于挂起和唤醒线程(LockSupport.park()会让线程进入WATING状态)。 AQS中对线程进行挂起和唤醒操作最终使用的就是LockSupport.park(xxx);LockSupport.unpark(xxx)

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类能够直接操作这些字段,实现对线程状态的控制。

  • 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 会记录阻塞线程的锁对象,这也有利于调试。

LockSupport 类的常用方法

挂起线程的相关方法

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);
}

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(Object blocker)parkNanos(Object blocker, long nanos)parkUntil(Object blocker, long deadline)三个方法参数中都有一个blocker,这个blocker是用来记录线程被阻塞时被谁阻塞的。用于线程监控和分析工具来定位原因。

唤醒线程的相关方法

unpark(Thread thread)

unpark方法用于唤醒被park方法挂起的线程。

public static void unpark(Thread thread) {
// 检查传入的线程是否为 null
if (thread != null) {
// 使用 UNSAFE 类的 unpark 方法唤醒指定的线程
UNSAFE.unpark(thread);
}
}

unpark会唤醒被park的指定线程。但是,这里要说明的是,unpark 并不是简单的直接去唤醒被park的线程。

unpark只是给当前线程设置一个许可证。如果当前线程已经被阻塞了(即调用了park),则会转为不阻塞的状态。如若不然,下次调用park方法的时候也会保证不阻塞。这句话的意思,其实是指,park和unpark的调用顺序无所谓,只要unpark设置了这个许可证,park方法就可以在任意时刻消费许可证,从而不会阻塞方法。

还需要注意的是,许可证最多只有一个,也就是说,就算unpark方法调用多次,也不会增加许可证。

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方法不一定生效。

看下面的例子:

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使用示例

先unpark再park

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而不是ifLockSupport源码中注释也是这么建议的:

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检查条件了,会让代码变得臃肿且难以理解和维护。

先interrupt再park

public class LockSupportTest1 {
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println(getName() + "进入线程");
LockSupport.park();
System.out.println("t1线程运行结束");
System.out.println("是否中断:" + Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
t1.start();
System.out.println("t1已经启动,但是内部进行了park");
t1.interrupt();
System.out.println("main线程结束");
}
}

运行结果如下:

线程中断相关方法

线程中和中断相关的方法有三个,分别介绍如下:

1.interrupt

我们一般都说这个方法是用来中断线程的,那么这个中断应该怎么理解呢? 就是说把当前正在执行的线程中断掉,不让它继续往下执行吗?

其实,不然。 此处,说的中断仅仅是给线程设置一个中断的标识(设置为true),线程还是会继续往下执行的。而线程怎么停止,则需要由我们自己去处理。

2.isInterrupted

判断当前线程的中断状态,即判断线程的中断标识是true还是false。 注意,这个方法不会对线程原本的中断状态产生任何影响。

3.interrupted

也是判断线程的中断状态的。但是,需要注意的是,这个方法和 isInterrupted 有很大的不同。我们看下它们的源码:

public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
//调用同一个方法,只是传参不同
private native boolean isInterrupted(boolean ClearInterrupted);

首先 isInterrupted 方法是线程对象的方法,而 interrupted 是Thread类的静态方法。

其次,它们都调用了同一个本地方法 isInterrupted,不同的只是传参的值,这个参数代表的是,是否要把线程的中断状态清除(清除即不论之前的中断状态是什么值,最终都会设置为false)。

因此,interrupted 静态方法会把原本线程的中断状态清除,而 isInterrupted 则不会。所以,如果你调用两次 interrupted 方法,第二次就一定会返回false,除非中间又被中断了一次。

sleep响应中断

线程中常用的阻塞方法,如:sleepjoinwait 都会响应中断,然后抛出一个中断异常 InterruptedException。但是,注意此时,线程的中断状态会被清除。所以,当我们捕获到中断异常之后,应该保留中断信息,以便让上层代码知道当前线程中断了。通常有两种方法可以做到。

一种是,捕获异常之后,再重新抛出异常,让上层代码知道。另一种是,在捕获异常时,通过 interrupt 方法把中断状态重新设置为true。

下面,就以sleep方法为例,捕获中断异常,然后重新设置中断状态:

public class InterruptTest1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
private int count = 0;
@Override
public void run() {
count = new Random().nextInt(1000);
count = count * count;
System.out.println("count:" + count);
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + "线程第一次中断标志:" + Thread.currentThread().isInterrupted());
// 重新把线程中断状态设置为true。
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + "线程第二次中断标志:" + Thread.currentThread().isInterrupted());
}
}
});
t.start();
Thread.sleep(100);
t.interrupt();
}
}

结果如下:

按我理解,sleep() 方法会不断的判断线程是否被中断过,若检测到中断标志则会抛出异常InterruptedException

park 和 interrupt 中断

park方法可以阻塞当前线程,如果调用unpark方法或者中断当前线程,则会从park方法中返回。

park方法对中断方法的响应和 sleep 有一些不太一样。它不会抛出中断异常,而是从park方法直接返回,不影响线程的继续执行。我们看下代码:

public class LockSupportTest2 {
static class ParkThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始阻塞");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "第一次结束阻塞");
LockSupport.park();
System.out.println("第二次结束阻塞");
}
}
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new ParkThread());
t.start();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "开始唤醒阻塞线程");
t.interrupt();
System.out.println(Thread.currentThread().getName() + "结束唤醒");
}
}

打印结果如下:

当调用interrupt方法时,会把中断状态设置为true,然后park方法会去判断中断状态,如果为true,就直接返回,然后往下继续执行,并不会抛出异常。注意,这里并不会清除中断标志。只要线程有中断标志,park() 就不再阻塞线程,不论调用多少次park() ,都不会阻塞线程。

  • 总结

LockSupport类提供了多种挂起线程的方法,主要是通过Unsafe类的park方法实现。挂起线程的方法分为带阻塞对象的和不带阻塞对象的,带阻塞对象的方法在挂起线程时会记录阻塞的原因或对象,以便调试和监视。每种方法还支持不同的挂起时长,包括无限期挂起、挂起指定的纳秒数和挂起直到某个时间点。通过这些方法,LockSupport类为高级并发控制提供了基础设施。比如AQS以及依赖AQS为基础实现的锁或者同步器,最终都是通过LockSupport提供的park和unpark实现的线程挂起和唤醒。

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 而不必担心线程是否已经阻塞。

总结下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_lockpthread_cond_wait 等函数来实现这些功能。

park\unpark VS wait\notify\sleep\await\singnal对比

posted @   Lz_蚂蚱  阅读(27)  评论(0编辑  收藏  举报
(评论功能已被禁用)
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起