0
点赞
收藏
分享

微信扫一扫

Java 并发编程(一) → LockSupport 详解

开心一刻

  今天突然收到花呗推送的消息,说下个月 9 号需要还款多少钱

  我就纳了闷了,我很长时间没用花呗了,怎么会欠花呗钱?

  后面我一想,儿子这几天玩了我手机,是不是他偷摸用了我的花呗

  于是我找到儿子问了起来

  我:儿子,你是不是用了我的花呗

  儿子:是的呀,爸,我就用了一点

  我:额度就剩两块了,你用了我用什么?

  儿子:你用你爸的呗!

  我:...

  不对呀,我女朋友都没有,哪里的儿子?猛的被惊醒,大白天的,我特么竟然还做上了白日梦!

Java 并发编程(一) → LockSupport 详解_java

前言

  本文是基于 JDK1.8

  那么此时 Java 线程与操作系统线程的对应关系是 1:1 的,有兴趣的可以读一读:​​深入聊聊java线程模型现?​​

  至于 Java 是否在未来引入类似 Go 中的协程,从而实现 Java 线程与操作系统线程的关系是 m:n,那是未来的事,那就未来再说

  我们能确定的是:Java8 中,Java 线程与操作系统线程是 1:1 的

LockSupport 简介

  关于 LockSupport,我们对它感到很陌生,因为我们在工作中很少直接接触到它,但多多少少,我们都间接用到过它

  LockSupport 是 JUC 包下很重要的一个工具类,我们来看看它的源码概述:

    Basic thread blocking primitives for creating locks and other synchronization classes

    用于创建锁和其他同步类的基本线程阻塞原语

  JUC 包下的锁、同步类基本都依赖 LockSupport 实现线程的阻塞与唤醒

  我们可以简单的认为 LockSupport 对 Java 线程(操作系统线程)的阻塞与唤醒进行了封装,简化了开发人员的任务

  permit(许可证)

  LockSupport 的设计思路就是为每一个线程设置一个 permit,其实就是一个值,类似于 AQS 中的 state

private volatile int

    permit 默认值(初始值)是 0,permit 最小值是 0,最大值是 1;0 表示许可证不可用,1 表示许可证可用

    若 permit 值为 0,则 park 方法会阻塞当前线程,直至超时或有可用的 permit;若 permit 为 1 ,则 park 方法会将 permit 值设置成 0,不会阻塞当前线程

    不管 permit 的值是 0 还是 1,unpark 方法会将 permit 设置成 1,也就说多次 unpark (中间没有 park)后,permit 的值仍是 1

  那么问题来了,permit 不在 LockSupport 中,那么它在哪?

/hotspot/src/share/vm/runtime/park.hpp

class Parker : public os::PlatformParker {
private:
volatile int _counter ;
Parker * FreeNext ;
JavaThread * AssociatedWith ; // Current association

public:
Parker() : PlatformParker() {
_counter = 0 ;
FreeNext = NULL ;
AssociatedWith = NULL ;
}
protected:
~Parker() { ShouldNotReachHere(); }
public:
// For simplicity of interface with Java, all forms of park (indefinite,
// relative, and absolute) are multiplexed into one call.
void park(bool isAbsolute, jlong time);
void unpark();

// Lifecycle operators
static Parker * Allocate (JavaThread * t) ;
static void Release (Parker * e) ;
private:
static Parker * volatile FreeList ;
static volatile int ListLock ;

};

View Code

volatile int

LockSupport 核心方法

  方法不多,如下图

  

Java 并发编程(一) → LockSupport 详解_LockSupport_02

park 和 unpark

  park

  会消耗 permit,若当前没有可用的 permit,则会阻塞当前线程

  park()

    方法体非常简单

Java 并发编程(一) → LockSupport 详解_示例代码_03

UNSAFE.park(false, 0L); 关于 Unsafe,有兴趣的可以去了解下:​​Java魔法类:Unsafe应用解析​​

    只看这个代码,我们很难看出什么,所幸有方法注释,简单翻译一下

    1、除非 permit 可用,否则阻塞当前线程直至 permit 可用

    2、如果 permit 可用,会将 permit 设置成 0,立即返回,不会阻塞当前线程

    3、当 permit 不可用时,当前线程会被阻塞,直至发生以下三种情况

      3.1 其他线程调用 unpark 唤醒此线程

Thread#interrupt

      3.3 该调用不合逻辑地(即毫无理由地)返回,可能是操作系统异常导致的

    4、park() 不会报告是什么原因导致的调用返回,有需要的话,调用者需在返回时自行检查是什么条件导致调用返回

  park(Object blocker)

    方法体也很简单

Java 并发编程(一) → LockSupport 详解_原语_04

Object blocker

    我们通过 jstack 命令,来看看 park() 和 park(Object blocker) 线程快照信息有什么区别

    示例代码:

Java 并发编程(一) → LockSupport 详解_原语_05

    用 park() 时线程 t1 的快照信息如下

Java 并发编程(一) → LockSupport 详解_原语_06

    用 park(Object blocker) 时线程 t1 的快照信息如下

Java 并发编程(一) → LockSupport 详解_示例代码_07

- parking to wait for

    当然 park(Object blocker) 不会像示例中那么使用(传个固定的字符串),传的肯定是有意义的对象,我们来看看 JDK 中哪些地方用到了它

Java 并发编程(一) → LockSupport 详解_线程阻塞与唤醒_08

    感兴趣的可以去看看具体的代码,其中的 this 具体是什么,它作为 blocker 有什么作用

  parkNanos(long nanos)

Java 并发编程(一) → LockSupport 详解_原语_09

等待的最大纳秒数;我们来翻译一下方法的注释

    1、除非 permit 可用,否则阻塞当前线程直至 permit 可用,或者等待的时间结束

    2、如果 permit 可用,会将 permit 设置成 0,立即返回,不会阻塞当前线程

四种情况

      3.1 其他线程调用 unpark 唤醒此线程

Thread#interrupt 中断此线程

3.3 经过指定的等待时间,不会无限期的等待下去

      3.4 该调用不合逻辑地(即毫无理由地)返回,可能是操作系统异常导致的

    4、park() 不会报告是什么原因导致的调用返回,有需要的话,调用者需在返回时自行检查是什么条件导致调用返回

    可以看出,功能与 park() 基本一致,只是多了一个等待时长

  parkNanos(Object blocker, long nanos)

Java 并发编程(一) → LockSupport 详解_线程阻塞与唤醒_10

Object blocker

    将 parkNanos(Object blocker, long nanos) 与 parkNanos(long nanos)  的关系与 park(Object blocker) 于 park() 的关系进行类比,就好理解了

    JDK 中有很多地方用到了 parkNanos(Object blocker, long nanos)

Java 并发编程(一) → LockSupport 详解_LockSupport_11

  parkUntil(long deadline)

Java 并发编程(一) → LockSupport 详解_java_12

等待到的绝对时间,以毫秒为单位

只是 parkNanos(long nanos) 等待的是相对时长(纳秒),而 parkUntil(long deadline) 等待的则是绝对时间点(毫秒)

  parkUntil(Object blocker, long deadline)

Java 并发编程(一) → LockSupport 详解_原语_13

Object blocker

    将 parkUntil(Object blocker, long deadline) 与 parkUntil(long deadline) 的关系与 parkNanos(Object blocker, long nanos) 与 parkNanos(long nanos)  的关系进行列表,就好理解了

    JDK 中有些地方用到了 parkUntil(Object blocker, long deadline) 

Java 并发编程(一) → LockSupport 详解_原语_14

  unpark

  方法体非常简单

Java 并发编程(一) → LockSupport 详解_示例代码_15

  我们来翻一下它的注释

  1、使入参线程的 permit 可用(将 permit 设置成 1)

  2、如果入参线程正阻塞于 park,那么会唤醒入参线程,否则入参线程的下一次 park 不会阻塞

  3、如果入参线程还没有启动,它不会产生任何效果

  4、如果入参线程为null,它不会产生任何效果

  JDK 中有很多地方用到了它

Java 并发编程(一) → LockSupport 详解_java_16

使用场景

基本不会直接使用

  其实不然,只要我们用到 JUC 下的类来进行并发编程,那么就已经间接用到了 LockSupport 了

  JUC 中线程的阻塞与唤醒的实现,依赖的都是 LockSupport

  线程交替打印

    这是楼主之前遇到的一个面试题,LockSupport 就是其中的一个考点,具体可查看:​​记一个有意思的面试题 → 线程交替输出问题​​

    用 LockSupport 是最优的解决方式,不依赖于第三方的同步值,代码简单,逻辑清晰,非常好理解和实现

总结

  1、park 分三类,每类分两种,官方推荐用带 blocker 参数的那一种

park(Object blocker)

parkNanos(Object blocker, long nanos)

parkUntil(Object blocker, long deadline)

  2、park 与 unpark 之间没有严格的调用先后顺序

    permit = 1 表示可用,permit = 0 表示不可用;permit 属于线程私有

    park 消耗 permit,将 permit 从 1 设置成 0;unpark 则将 permit 设置成 1,不管设置前的值是 1 还是 0

    permit 可用,则 park 不会阻塞当前线程,将 permit 设置成 0,线程继续往下执行,否则 park 会阻塞当前线程

    unpark 会设置指定线程的 permit = 1,并唤醒指定的线程

参考

  ​​Java魔法类:Unsafe应用解析​​

  ​​JVM 常见线上问题 → CPU 100%、内存泄露 问题排查​​



举报

相关推荐

0 条评论