1. LockSupport 简介

ReentrantLockReentReadWriteLocks 以及在介绍线程间等待/通知机制使用的 Condition 时都会调用 LockSupport.park() 方法和 LockSupport.unpark() 方法。而这个在同步组件的实现中被频繁使用的 LockSupport 到底是何方神圣?

LockSupport 位于 java.util.concurrent.locks 包下。LockSupport 和每个使用它的线程都与一个许可 (permit)关联。permit 相当于1,0的开关,默认是 0,调用一次 unpark 就 +1 变成 1,调用一次 park 会消费 permit, 也就是将 1 变成 0,同时 park 立即返回。再次调用 park 会变成 block(因为 permit 为0了,会阻塞在这里,直到 permit 变为 1), 这时调用 unpark 会把 permit 置为1。每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 也不会积累。

park()unpark() 不会有 Thread.suspendThread.resume 所可能引发的死锁”问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。如果调用线程被中断,则 park方法会返回。同时 park 也拥有可以设置超时时间的版本。

需要特别注意的一点:park 方法还可以在其他任何时间「毫无理由」地返回,因此通常必须在重新检查返回条件的循环里调用此方法。从这个意义上说,park 是「忙碌等待」的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高效。

LockSupport 很类似于二元信号量(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。底层是通过调用本地代码(C++)来做的,具有很强的OS平台相关性,因此性能应该是非常高的。

:::info park 翻译过来即是停车的意思,我们可以这样理解,每个被应用程序启动的线程就是一辆在计算机总线赛道上奔驰着的跑车,当你想让某台车停下来休息会时那么就给它一个 park 信号,它就会立即停到赛道旁边的停车位中,当你想让它从停车位中驶出并继续在赛道上奔跑时再给它一个 unpark 信号即可 :::

2. LockSupport 方法介绍

阻塞线程

  1. void park():阻塞当前线程,如果调用 unpark 方法或者当前线程被中断,能从park()方法中返回
  2. void park(Object blocker):功能同方法1,入参增加一个 Object 对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
  3. void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性;
  4. void parkNanos(Object blocker, long nanos):功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
  5. void parkUntil(long deadline):阻塞当前线程,直到 deadline;
  6. void parkUntil(Object blocker, long deadline):功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;

唤醒线程

  1. void unpark(Thread thread):唤醒处于阻塞状态的指定线程

实际上 LockSupport 阻塞和唤醒线程的功能是依赖于 sun.misc.Unsafe,这是一个很底层的类,有兴趣的可以去查阅资料,比如 park() 方法的功能实现则是靠 unsafe.park() 方法。另外在阻塞线程这一系列方法中还有一个很有意思的现象就是,每个方法都会新增一个带有 Object 的阻塞对象的重载方法。那么增加了一个Object对象的入参会有什么不同的地方了?示例代码很简单就不说了,直接看 dump 线程的信息。

调用**park()**方法dump线程

  1. "main" #1 prio=5 os_prio=0 tid=0x02cdcc00 nid=0x2b48 waiting on condition [0x00d6f000]
  2. java.lang.Thread.State: WAITING (parking)
  3. at sun.misc.Unsafe.park(Native Method)
  4. at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
  5. at learn.LockSupportDemo.main(LockSupportDemo.java:7)

调用**park(Object blocker)**方法 dump 线程

  1. "main" #1 prio=5 os_prio=0 tid=0x0069cc00 nid=0x6c0 waiting on condition [0x00dcf000]
  2. java.lang.Thread.State: WAITING (parking)
  3. at sun.misc.Unsafe.park(Native Method)
  4. - parking to wait for <0x048c2d18> (a java.lang.String)
  5. at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
  6. at learn.LockSupportDemo.main(LockSupportDemo.java:7)

通过分别调用这两个方法然后 dump 线程信息可以看出,带 Object 的 park 方法相较于无参的 park 方法会增加 parking to wait for <0x048c2d18> (a java.lang.String)的信息,这种信息就类似于记录「案发现场」,有助于工程人员能够迅速发现问题解决问题。有个有意思的事情是,我们都知道如果使用 synchronzed 阻塞了线程 dump 线程时都会有阻塞对象的描述,在java 5推出 LockSupport时遗漏了这一点,在java 6时进行了补充。还有一点需要需要的是:synchronzed 致使线程阻塞,线程会进入到 BLOCKED 状态,而调用 LockSupprt 方法阻塞线程会致使线程进入到 WAITING 状态。

3. 一个例子

用一个很简单的例子说说这些方法怎么用。

  1. public class LockSupportDemo {
  2. public static void main(String[] args) {
  3. Thread thread = new Thread(() -> {
  4. LockSupport.park();
  5. System.out.println(Thread.currentThread().getName() + "被唤醒");
  6. });
  7. thread.start();
  8. try {
  9. Thread.sleep(3000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. LockSupport.unpark(thread);
  14. }
  15. }

thread 线程调用 LockSupport.park() 致使 thread 阻塞,当 mian 线程睡眠3秒结束后通过LockSupport.unpark(thread) 方法唤醒 thread 线程,thread 线程被唤醒执行后续操作。另外,还有一点值得关注的是,**LockSupport.unpark(thread)** 可以指定线程对象唤醒指定的线程

资料

  • 业务场景下的使用实例:https://www.jianshu.com/p/84be6bd90727
    • 为了避免数据库资源过度使用的问题我的设计是在web容器后台构建一块监控数据缓存,无论前台有多少个人访问监控页面,都只是从web容器缓存中获取监控数据,web容器后台有一个值守线程X每间隔一秒访问数据库轮询监控数据至内存中