LockSupport类为构建锁和同步器提供了基本的线程阻塞唤醒原语,JDK中我们熟悉的AQS基础同步类就使用了它来控制线程的阻塞和唤醒,也许我们更加熟悉的阻塞唤醒操作是wait/notify方式,它主要以Object的角度来设计。而LockSupport提供的park/unpark则是以线程的角度来设计,真正解耦了线程之间的同步。
许可机制
线程成功拿到许可则能够往下执行,否则将进入阻塞状态。对于LockSupport使用的许可可看成是一种二元信号,该信号分有许可和无许可两种状态。每个线程都对应一个信号变量,当线程调用park时其实就是去获取许可,如果能成功获取到许可则能够往下执行,否则则阻塞直到成功获取许可为止。而当线程调用unpark时则是释放许可,供线程去获取
public class LockSupportTest {private static boolean isLocked = true;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {while (isLocked) {System.out.println(Thread.currentThread().getName() + " is locked");LockSupport.park();}System.out.println(Thread.currentThread().getName() + " finish");}, "thread-1");Thread thread2 = new Thread(() -> {isLocked = false;LockSupport.unpark(thread1);System.out.println(Thread.currentThread().getName() + " unpark thread1");}, "thread-2");thread1.start();TimeUnit.SECONDS.sleep(2);thread2.start();}}// console result
thread-1 is locked
thread-2 unpark thread1
thread-1 finish
需要注意的是在调用LockSupport的park方法时一般会使用while(condition)循环体,如下方框的代码所示,这样能保证在线程被唤醒后再一次判断条件是否符合。
核心方法
该类主要包含两种操作,分别为阻塞操作和唤醒操作
public class LockSupport {private LockSupport() {} // Cannot be instantiated.// 设置当前线程等待的对象(下文简称为阻塞对象),该对象主要用于问题排查和系统监控private static void setBlocker(Thread t, Object arg) {UNSAFE.putObject(t, parkBlockerOffset, arg);}// 唤醒一个由 park 方法阻塞的线程。如果该线程未被阻塞,那么下一次调用 park 时将立即返回。这允许“先发制人”式的唤醒机制。public static void unpark(Thread thread) {if (thread != null)UNSAFE.unpark(thread);}// 阻塞当前线程,如果调用 unpark 方法或线程被中断,则该线程将变得可运行。请注意,park 不会抛出 InterruptedException,因此线程必须单独检查其中断状态。// 入参用来记录导致线程阻塞的对象,方便问题排查。public static void park(Object blocker) {Thread t = Thread.currentThread();setBlocker(t, blocker);UNSAFE.park(false, 0L);setBlocker(t, null);}// 阻塞当前线程一定的纳秒时间,或直到被 unpark 调用,或线程被中断。// 入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查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);}}// 阻塞当前线程直到某个指定的截止时间(以毫秒为单位),或直到被 unpark 调用,或线程被中断。// 入参增加一个 Object 对象,用来记录导致线程阻塞的对象,方便问题排查public static void parkUntil(Object blocker, long deadline) {Thread t = Thread.currentThread();setBlocker(t, blocker);UNSAFE.park(true, deadline);setBlocker(t, null);}public static Object getBlocker(Thread t) {if (t == null)throw new NullPointerException();return UNSAFE.getObjectVolatile(t, parkBlockerOffset);}public static void park() {UNSAFE.park(false, 0L);}public static void parkNanos(long nanos) {if (nanos > 0)UNSAFE.park(false, nanos);}public static void parkUntil(long deadline) {UNSAFE.park(true, deadline);}static final int nextSecondarySeed() {int r;Thread t = Thread.currentThread();if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {r ^= r << 13; // xorshiftr ^= r >>> 17;r ^= r << 5;}else if ((r = java.util.concurrent.ThreadLocalRandom.current().nextInt()) == 0)r = 1; // avoid zeroUNSAFE.putInt(t, SECONDARY, r);return r;}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 = sun.misc.Unsafe.getUnsafe();Class<?> tk = Thread.class;parkBlockerOffset = UNSAFE.objectFieldOffset(tk.getDeclaredField("parkBlocker"));SEED = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSeed"));PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));SECONDARY = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSecondarySeed"));} catch (Exception ex) { throw new Error(ex); }}
}
LockSupport 中的 blocker
private static final sun.misc.Unsafe UNSAFE;private static final long parkBlockerOffset;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class<?> tk = Thread.class;parkBlockerOffset = UNSAFE.objectFieldOffset(tk.getDeclaredField("parkBlocker"));} catch (Exception ex) { throw new Error(ex); }}private static void setBlocker(Thread t, Object arg) {// Even though volatile, hotspot doesn't need a write barrier here.UNSAFE.putObject(t, parkBlockerOffset, arg);}public
class Thread implements Runnable {/*** The argument supplied to the current call to* java.util.concurrent.locks.LockSupport.park.* Set by (private) java.util.concurrent.locks.LockSupport.setBlocker* Accessed using java.util.concurrent.locks.LockSupport.getBlocker*/volatile Object parkBlocker;}
`tk.getDeclaredField("parkBlocker")` 就是获取线程中的parkBlocker
字段
'UNSAFE.objectFieldOffset()' 获取某个字段相对 Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java 对象的某个字段
` UNSAFE.putObject(t, parkBlockerOffset, arg) ` 该方法有三个参数,第一个参数是操作对象,第二个参数是内存偏移量,第三个参数是实际存储值。该方法理解起来也很简单,就是操作某个对象中某个内存地址下的数据
Dump 线程
"Dump 线程"通常是指获取线程的当前状态和调用堆栈的详细快照。这可以提供关于线程正在执行什么操作以及线程在代码的哪个部分的重要信息。
下面是线程转储中可能包括的一些信息:
- 线程 ID 和名称:线程的唯一标识符和可读名称。
- 线程状态:线程的当前状态,例如运行(RUNNABLE)、等待(WAITING)、睡眠(TIMED_WAITING)或阻塞(BLOCKED)。
- 调用堆栈:线程的调用堆栈跟踪,显示线程从当前执行点回溯到初始调用的完整方法调用序列。
- 锁信息:如果线程正在等待或持有锁,线程转储通常还包括有关这些锁的信息。
线程转储可以通过各种方式获得,例如使用 Java 的 jstack 工具,或从 Java VisualVM、Java Mission Control 等工具获取。
下面是一个简单的例子,通过 LockSupport 阻塞线程,然后通过 Intellij IDEA 查看 dump 线程信息。
park()
public class LockSupportTest {public static void main(String[] args) {LockSupport.park();}private static class User{}
}
运行程序
调用 park()方法 dump 线程:
"main" #1 prio=5 os_prio=0 tid=0x000001b2caaf9800 nid=0x313c waiting on condition [0x0000009afc6ff000]java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)at LockSupportTest.main(LockSupportTest.java:11)
park(Object blocker)
public class LockSupportTest {public static void main(String[] args) {LockSupport.park(new User());}private static class User{}
}
调用 park(Object blocker)方法 dump 线程
"main" #1 prio=5 os_prio=0 tid=0x0000024d21c7a000 nid=0x5638 waiting on condition [0x0000006fa98ff000]java.lang.Thread.State: WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for <0x000000076b5a9120> (a LockSupportTest$User)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)at LockSupportTest.main(LockSupportTest.java:11)
park 和 unpark 的执行顺序
正常来说一般是先park线程阻塞,然后unpark来唤醒该线程。但是如果先执行unpark再执行park,会不会park就永远没办法被唤醒了呢?
在往下分析之前我们先来看看wait/notify方式的执行顺序例子。主线程分别创建了thread1和thread2,然后启动它们。由于thread1会延迟3s再启动,所以先由thead2执行了notify去唤醒阻塞在锁对象的线程,而在三秒后thread1才会执行wait方法,此时它将一直阻塞在那里。所以wait/notify方式的执行顺序会影响到唤醒。
wait 和 notify 的顺序
public class LockSupportTest {private static Boolean isLocked = true;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {synchronized (isLocked) {try {System.out.println(Thread.currentThread().getName() + " isLocked.wait()");isLocked.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName() + " finish");}, "thread-1");Thread thread2 = new Thread(() -> {synchronized (isLocked) {isLocked.notify();System.out.println(Thread.currentThread().getName() + " isLocked.notify()");}}, "thread-2");thread2.start();TimeUnit.SECONDS.sleep(2);thread1.start();}}
控制台输出
park 与 unpark 的顺序
public class LockSupportTest {private static Boolean isLocked = true;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " isLocked=" + isLocked);LockSupport.park();System.out.println(Thread.currentThread().getName() + " finish");}, "thread-1");Thread thread2 = new Thread(() -> {isLocked = false;LockSupport.unpark(thread1);System.out.println(Thread.currentThread().getName() + " unpark thread1");}, "thread-2");thread2.start();// TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName() + " thread1.start()");thread1.start();}}// console result
main thread1.start()
thread-2 unpark thread1
thread-1 isLocked=false
thread-1 finish
注意:这里不能在 main 线程进行 sleep,因为 thread1 线程尚未启动(start()
没有被调用),那么 LockSupport.unpark
操作不会对这个线程产生影响,因为这个线程还不存在于 JVM 的线程管理中,也就无法对其应用 unpark
操作。
因此,为了确保 LockSupport.unpark
能够正常工作,必须确保线程已经被创建并且已经通过 Thread.start()
方法启动。只有这样,JVM 才能管理这个线程,并允许 LockSupport
类的操作对其产生作用。如果线程还没有启动,你需要先启动线程,然后再根据需要使用 LockSupport.park
和 LockSupport.unpark
来控制它的阻塞与解除阻塞。
Park 对中断的响应
park方法支持中断,也就是说一个线程调用park方法进入阻塞后,如果该线程被中断则能够解除阻塞立即返回。但需要注意的是,它不会抛出中断异常,所以我们不必去捕获InterruptedException
public class LockSupportTest {public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + " park");LockSupport.park();System.out.println(Thread.currentThread().getName() + " finish");}, "thread-1");thread1.start();TimeUnit.SECONDS.sleep(2);System.out.println(Thread.currentThread().getName() + " sleep two second later");thread1.interrupt();}
}// console result
thread-1 park
main sleep two second later
thread-1 finish
park 是否释放锁
public class LockSupportTest {private static Boolean isLocked = true;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {synchronized (isLocked) {System.out.println(Thread.currentThread().getName() + " LockSupport.park()");LockSupport.park();}System.out.println(Thread.currentThread().getName() + " finish");}, "thread-1");Thread thread2 = new Thread(() -> {synchronized (isLocked) {LockSupport.unpark(thread1);System.out.println(Thread.currentThread().getName() + " LockSupport.unpark(thread1)");}}, "thread-2");thread1.start();TimeUnit.SECONDS.sleep(2);thread2.start();}
}
控制台输出
thread1和thread2两个线程都通过synchronized(lock)来获取锁。由于thread1先启动所以获得锁,而且它调用park方法使得thread1进入阻塞状态,但是thread1却不会释放锁lock。对于thread2这边,因为一直无法获取锁而无法进入同步块,也就没办法执行unpark操作。最终的结果就是造成了死锁,thread1在等thread2的unpark,而thread2却在等thread1释放锁。
总结
LockSupport.park()休眠线程,LockSupport.unpark()唤醒线程,两个方法配合使用。也可以通过LockSupport.parkNanos()指定休眠时间后,自动唤醒。
LockSupport.park()不会释放monitor锁。
线程被打断,LockSupport.park()不会抛出异常,也不会吞噬掉interrupt的状态,调用者可以获取interrupt状态,自行进行判断,线程是由于什么原因被唤醒了。
LockSupport.park()会是线程进入WAITING状态,而LockSupport.parkNanos(long nanos) 会进入TIMED_WAITING状态。
LockSupport.park(Object blocker)和LockSupport.getBlocker(t1)配合使用,可以进行自定义数据传输。
深入理解Java并发线程阻塞唤醒类LockSupport | 二哥的Java进阶之路
并发锁LockSupport原理剖析,四千字多图讲解+多例子+代码分析-腾讯云开发者社区-腾讯云