LockSupport
并发锁LockSupport原理剖析,四千字多图讲解+多例子+代码分析
LockSupport简介
LockSupport
是 Java 并发包(java.util.concurrent)中的一个工具类,是 Java 并发编程中一个非常重要的组件,提供了基本的线程阻塞和唤醒机制。我们熟知的并发组件 Lock、线程池、CountDownLatch
等都是基于 AQS
实现的,而 AQS
内部控制线程阻塞和唤醒又是通过 LockSupport
来实现的。
从该类的注释上也可以发现,它是一个控制线程阻塞和唤醒的工具,与以往的不同是它解决了曾经 wait()、notify()、await()、signal()
的局限。
LockSupport 提供的主要方法是 park
和 unpark
,它们分别用于挂起和唤醒线程(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而不是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
检查条件了,会让代码变得臃肿且难以理解和维护。
先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响应中断
线程中常用的阻塞方法,如:sleep
,join
和wait
都会响应中断,然后抛出一个中断异常 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_lock
和 pthread_cond_wait
等函数来实现这些功能。
park\unpark
VS wait\notify\sleep\await\singnal
对比
本文来自博客园,作者:Lz_蚂蚱,转载请注明原文链接:https://www.cnblogs.com/leizia/p/18449892
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步