阅读本文前,需要储备的知识点如下,点击链接直接跳转。

java线程详解

Java不能操作内存?Unsafe了解一下

LockSupport介绍

搞java开发的基本都知道J.U.C并发包(即java.util.concurrent包),所有并发相关的类基本都来自于这个包下,这个包是JDK1.5以后由祖师爷Doug Lea写的,LockSupport也是在这时诞生的,在JDK1.6又加了些操作方法。

其实LockSupport的这些静态方法基本都是调用Unsafe类的方法,所以建议大家看看文章开头的Unsafe那篇文章。

首先我们来看看LockSupport类开头的一段注释。

/**
* Basic thread blocking primitives for creating locks and other
* synchronization classes.
*
* <p>This class associates, with each thread that uses it, a permit
* (in the sense of the {@link java.util.concurrent.Semaphore
* Semaphore} class). A call to {@code park} will return immediately
* if the permit is available, consuming it in the process; otherwise
* it <em>may</em> block. A call to {@code unpark} makes the permit
* available, if it was not already available. (Unlike with Semaphores
* though, permits do not accumulate. There is at most one.)

大概的意思就是说,LockSupport这个类用于创建锁和其他同步类的基本线程阻塞原语。这个类与使用它的每个线程关联一个许可证,这个许可证数量不会累积。最多只有一个即permit要么是0要么是1,如果调用park方法,permit=1时则当前线程继续执行,否则没有获取到许可证,阻塞当前线程;调用unpark方法会释放一个许可,把permit置为1,连续多次调用unpark只会把许可证置为1一次,被阻塞的线程获取许可后继续执行。

额,可能刚开始接触这个类的童鞋有点懵逼,不过没关系,下面我为大家准备了饮料小菜花生米,诸位搬好小板凳,静静的听我吹牛逼吧,哈哈。

API

LockSupport类只有几个静态方法,构造方法是私有的,所以使用的过程中就调用它的这几个静态方法就够了。

  • 单纯的设置和获取阻塞对象。
// 给线程t设置阻塞对象为arg,以便出问题时排查阻塞对象,这个方法为私有方法,其他park的静态方法会调用这个方法设置blocker
private static void setBlocker(Thread t, Object arg)
// 获取线程t的阻塞对象,一般用于排查问题
public static Object getBlocker(Thread t)
  • 单纯的阻塞和给线程释放许可
// 阻塞当前线程,如果已经获取到许可则不阻塞继续执行,这个阻塞可以响应中断
public static void park()
// 释放线程thread的许可,使得thread线程从park处继续向后执行,如果threa为null不做任何操作
public static void unpark(Thread thread)
  • 只带时间的阻塞
// 阻塞线程,设置了等待超时时间,单位是纳秒,是相对时间,nanos<=0不会阻塞,相当于没有任何操作;nanos>0时,如果等待时间超过nanos纳秒还没有获取到许可,那么线程自动恢复执行
public static void parkNanos(long nanos)
// 这里的deadline单位是毫秒,而且是绝对时间,调用后会阻塞到指定的绝对时间如果还没有获取到许可则自动恢复执行
public static void parkUntil(long deadline)
  • 同时带阻塞对象和时间的阻塞
// 默认的许可permit=0,阻塞当前线程,并设置阻塞对象为blocker其实就是调用setBlocker这个私有方法。如果当前线程的permit=1了那么再调park是不会阻塞的,因为可以获取到许可继续执行。当前线程获取到许可后会清除blocker为null
public static void park(Object blocker)
// 作用同park(Object blocker)方法,唯一的区别就是设置了等待超时时间,单位是纳秒,是相对时间,nanos<=0不会阻塞,相当于没有任何操作;nanos>0时,如果等待时间超过nanos纳秒还没有获取到许可,那么线程自动恢复执行,例如nanos=1000*1000*1000,这个相当于1秒,等到1秒后如果还没有获取到许可醒则自动恢复
public static void parkNanos(Object blocker, long nanos)
// 作用同parkNanos(Object blocker, long nanos),设置阻塞对象blocker,但是这里的deadline单位是毫秒,而且是绝对时间,调用了parkUntil后会阻塞到指定的绝对时间如果还没有获取到许可则自动恢复执行
public static void parkUntil(Object blocker, long deadline)
  • 其他(别问,问我也不知道)
// 返回伪随机初始化或更新的辅助种子。由于包访问限制,从ThreadLocalRandom复制。PS:这是百度翻译的,平时用得少,我也没用过,暂且先放这里吧,用到了再细讲
static final int nextSecondarySeed()

关于LockSupport的park相关方法阻塞,有以下三种方法可获取到许可并继续向后执行。

  1. 主动调用unpark(Thread thread)方法,使得线程获得许可继续执行。
  2. 中断该线程即调用interrupt()方法,调用后线程不会抛出异常,直接从park的地方恢复过来继续执行
  3. 无原因的虚拟的返回,这种情况目前没有遇到过,不过在java.util.concurrent.locks.LockSupport.park()的注释里会有这种情况

使用案例

  • 基础的阻塞和释放许可
public static void test1() throws Exception {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " is running...");
// 调用park()方法会一直阻塞直到获得permit或者被中断
LockSupport.park();
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " get permit");
}
}, "t1");
t.start();
Thread.sleep(2000);
// 使得被阻塞的线程继续执行有三种方法
// 1、主动调用unpark(Thread thread)方法,使得线程获得许可继续执行
LockSupport.unpark(t);
// 2、中断该线程即调用interrupt()方法,调用后线程不会抛出异常,直接从park的地方恢复过来继续执行
// t.interrupt();
// 3、无原因的虚拟的返回,这种情况目前没有遇到过,不过在java.util.concurrent.locks.LockSupport.park()的注释里会有这种情况
}

输出如下:

2020-05-19 20:25:16:t1 is running...
2020-05-19 20:25:18:t1 get permit
  • 设置和获取阻塞对象
public static void test2() throws Exception {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " is running...");
// 设置线程的阻塞对象为一个字符串
LockSupport.park("i am blocker");
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " get permit");
}
}, "t1");
t.start();
Thread.sleep(2000);
// 获取t线程的阻塞对象,如果没有设置线程t的阻塞对象,则获取到的blocker是null
Object blocker = LockSupport.getBlocker(t);
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":" + Thread.currentThread().getName()
+ " get block class type:" + blocker.getClass());
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":" + Thread.currentThread().getName()
+ " get block value toString:" + blocker);
LockSupport.unpark(t);
}

输出:

2020-05-19 20:32:00:t1 is running...
2020-05-19 20:32:02:main get block class type:class java.lang.String
2020-05-19 20:32:02:main get block value toString:i am blocker
2020-05-19 20:32:02:t1 get permit
  • 带相对和绝对时间的阻塞
public static void test3() throws Exception {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " is running...");
// 设置线程的阻塞对象为一个字符串,并且阻塞3s,这里是相对时间,如有没有被unpark或者线程中断,3s后自动恢复执行
LockSupport.parkNanos("block1", TimeUnit.SECONDS.toNanos(3));
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " continue...");
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " is running...");
// 设置线程的阻塞对象为一个字符串,并且阻塞5s,这里使用的是绝对时间,只到当前时间+5s转换为毫秒,如有没有被unpark或者线程中断,绝对时间到后自动恢复执行
LockSupport.parkUntil("block2", System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5));
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " continue...");
}
}, "t2");
t2.start();
}

输出结果:

2020-05-20 08:35:07:t2 is running...
2020-05-20 08:35:07:t1 is running...
2020-05-20 08:35:10:t1 continue...
2020-05-20 08:35:12:t2 continue...
关于park阻塞的方法,针对于阻塞时间总结一下,有三种使用情况情况。
- 无限期阻塞,不带任何时间相关的参数,这种底层调用的是UNSAFE.park(false, 0L)。
- 相对时间阻塞,调用的parkNanos相关方法,这里的时间参数是一个相对时间,单位是纳秒,这种底层调用的是UNSAFE.park(false, nanos),表示经过nanos纳秒后如果还未获取到许可则自动恢复执行。
- 绝对时间阻塞,调用的parkUntil相关方法,这里的时间参数是一个绝对时间,单位是毫秒,这种底层调用的是UNSAFE.park(true, deadline),表示把当前时间换算成毫秒,如果值等于deadline毫秒后未获取到许可则自动恢复执行。

与对象锁比较

LockSupport与对象锁主要区别如下:

  1. 关注维度不同

    LockSupport是针对于线程级别的,而对象锁是synchronized关键字配合object对象的notify()、notifyAll()和wait()方法使用的,这种是针对于对象级别的。两者阻塞方式不同,我们看个栗子吧。
public static void test4() throws Exception {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
LockSupport.park();
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Object obj = new Object();
synchronized (obj) {
obj.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t2");
t2.start();
}

上面这段代码创建了两个线程,t1使用LockSupport.park()阻塞,t2使用obj.wait()阻塞,调用这个方法执行后,我们看看jvm的线程信息。

  • 先用jps找到对应的进程

  • 使用jstack查看线程信息

从这个dump的线程堆栈信息我们可以看出,t1和t2线程都处于WATING状态,但是t1是阻塞在了Unsafe.park方法上,parking状态,等待获取许可,t2是阻塞在Object.wait方法上,在等待一个object monitor即对象锁。

2. 唤醒方式不同

LockSupport是唤醒指定的线程,而notify()或者notifyAll()无法指定要唤醒的线程,只是表明对象上的锁释放了,让其他等待该锁的线程继续竞争锁,至于哪个线程先获取到锁是随机的,只是将获取到锁的线程由阻塞等待状态变成就绪状态,等待操作系统的调度才能继续执行。

3. 使用方式不同

LockSupport的park阻塞方式是在当前线程中执行并阻塞当前线程,但是唤醒unpark方法是在其他线程中执行的,并且唤醒后被park阻塞的方法能立即继续执行。但是notify或者notifyAll方法虽然调用后起到了通知释放对象锁的作用,但是他必须退出synchronized后才生效,下面我们分别看两个栗子。

LockSupport的park和unpark

public static void test5() throws Exception {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Thread lockSupportThread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " is go parking...");
LockSupport.park();
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " continue...");
}
}, "lockSupportThread1");
// 让lockSupportThread1线程先执行起来
lockSupportThread1.start();
Thread lockSupportThread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " is running...");
// 让当前线程休眠1s
Thread.sleep(1000);
// unpark线程lockSupportThread1
LockSupport.unpark(lockSupportThread1);
// 让当前线程休眠3s
Thread.sleep(3000);
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " over...");
} catch (Exception e) {
e.printStackTrace();
}
}
}, "lockSupportThread2");
lockSupportThread2.start();
}

输出:

2020-05-21 12:11:27:lockSupportThread2 is running...
2020-05-21 12:11:27:lockSupportThread1 is go parking...
2020-05-21 12:11:28:lockSupportThread1 continue...
2020-05-21 12:11:31:lockSupportThread2 over...

这里我们看到lockSupportThread2线程调用LockSupport.unpark后,虽然有休眠,但是lockSupportThread1线程还是立即执行了,说明LockSupport.unpark是立即释放线程许可。

接下来我们看下Object的wait()和notifyAll()。

public static void test6() {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Object object = new Object();
Thread lockSupportThread3 = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (object) {
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " is running...");
// 释放object锁让其他线程可以获得,当前线程阻塞
object.wait();
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " over...");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, "lockSupportThread3");
lockSupportThread3.start();
Thread lockSupportThread4 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 让当前线程休眠2s,确保lockSupportThread3先获取到object锁
Thread.sleep(2000);
synchronized (object) {
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " is running...");
// 让当前线程休眠1s
Thread.sleep(1000);
// 唤醒等待在object锁上的线程
object.notifyAll();
// 让当前线程休眠3s
Thread.sleep(3000);
System.out.println(LocalDateTime.now().format(dateTimeFormatter) + ":"
+ Thread.currentThread().getName() + " over...");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, "lockSupportThread4");
lockSupportThread4.start();
}

输出结果:

2020-05-21 12:16:51:lockSupportThread3 is running...
2020-05-21 12:16:53:lockSupportThread4 is running...
2020-05-21 12:16:57:lockSupportThread4 over...
2020-05-21 12:16:57:lockSupportThread3 over...

lockSupportThread4先休眠了2s确保lockSupportThread3先执行并获取到object对象锁,然后lockSupportThread3调用了object.wait(),释放object锁并线程阻塞等待,然后lockSupportThread4获取到了object锁继续执行,虽然lockSupportThread4在休眠和打印输出前调用了notifyAll方法,但是依然是lockSupportThread4的同步块代码执行完成后lockSupportThread3才开始执行。

总结

本文中虽然我们只介绍了LockSupport的API方法和使用案例,其实这也是除synchronized结合Object的wait()、notify()、notifyAll()来协调多线程同步的另一种方式。而且在只协调多线程的的情况下LockSupport会显得更灵活。

另外在jdk的并发包下,有各种锁,比如ReentrantLockCountDownLatchCyclicBarrier等,只要往底层看下源码,可以发现他们都使用了AbstractQueuedSynchronizer(简称AQS,抽象队列同步器,后续文章会专门介绍),而AbstractQueuedSynchronizer里的线程阻塞和唤醒正是使用的就是LockSupport,所以想要搞懂原理,就得把这些一一梳理清楚,最后自然而然就明白了。

说到这里,让我突然想起张三丰教张无忌学太极时的那一段对话。

张三丰:“无忌,我教你的还记得多少?”
张无忌:“回太师傅,我只记得一大半”
张三丰:“ 那,现在呢?”
张无忌:“已经剩下一小半了”
张三丰:“那,现在呢?”
张无忌:“我已经把所有的全忘记了!”
张三丰:“好,忘了好,刚才教你的都是错的,重新来吧...”
张无忌:......

emmmmm,好像走错片场了,那就江湖再见吧。。。

一文读懂LockSupport的更多相关文章

  1. 一文读懂HTTP/2及HTTP/3特性

    摘要: 学习 HTTP/2 与 HTTP/3. 前言 HTTP/2 相比于 HTTP/1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当然兼容问题以及如何 ...

  2. 一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现

    一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现 导读:近日,马云.马化腾.李彦宏等互联网大佬纷纷亮相2018世界人工智能大会,并登台演讲.关于人工智能的现状与未来,他们提出了各自的观点,也引 ...

  3. 一文读懂高性能网络编程中的I/O模型

    1.前言 随着互联网的发展,面对海量用户高并发业务,传统的阻塞式的服务端架构模式已经无能为力.本文(和下篇<高性能网络编程(六):一文读懂高性能网络编程中的线程模型>)旨在为大家提供有用的 ...

  4. 从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路

    本文原作者阮一峰,作者博客:ruanyifeng.com. 1.引言 HTTP 协议是最重要的互联网基础协议之一,它从最初的仅为浏览网页的目的进化到现在,已经是短连接通信的事实工业标准,最新版本 HT ...

  5. 一文读懂 深度强化学习算法 A3C (Actor-Critic Algorithm)

    一文读懂 深度强化学习算法 A3C (Actor-Critic Algorithm) 2017-12-25  16:29:19   对于 A3C 算法感觉自己总是一知半解,现将其梳理一下,记录在此,也 ...

  6. [转帖]MerkleDAG全面解析 一文读懂什么是默克尔有向无环图

    MerkleDAG全面解析 一文读懂什么是默克尔有向无环图 2018-08-16 15:58区块链/技术 MerkleDAG作为IPFS的核心数据结构,它融合了Merkle Tree和DAG的优点,今 ...

  7. [转帖]一文读懂 HTTP/2

    一文读懂 HTTP/2 http://support.upyun.com/hc/kb/article/1048799/ 又小拍 • 发表于:2017年05月18日 15:34:45 • 更新于:201 ...

  8. [转帖]从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路

    从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路   http://www.52im.net/thread-1709-1-2.html     本文原作者阮一峰,作者博客:r ...

  9. 一文读懂HDMI和VGA接口针脚定义

    一文读懂HDMI和VGA接口针脚定义 摘自:http://www.elecfans.com/yuanqijian/jiekou/20180423666604.html   HDMI概述 HDMI是高清 ...

  10. 即时通讯新手入门:一文读懂什么是Nginx?它能否实现IM的负载均衡?

    本文引用了“蔷薇Nina”的“Nginx 相关介绍(Nginx是什么?能干嘛?)”一文部分内容,感谢作者的无私分享. 1.引言   Nginx(及其衍生产品)是目前被大量使用的服务端反向代理和负载均衡 ...

随机推荐

  1. 2020-11-08:在Mysql中,三个字段A、B、C的联合索引,查询条件是B、A、C,会用到索引吗?

    福哥答案2020-11-08: 会走索引,原因是mysql优化器会把BAC优化成ABC. CREATE TABLE `t_testabc2` ( `id` int(11) NOT NULL AUTO_ ...

  2. 2021-02-27:假设一个固定大小为W的窗口,依次划过arr,返回每一次滑出状况的最大值。例如,arr = [4,3,5,4,3,3,6,7], W = 3。返回:[5,5,5,4,6,7]。

    2021-02-27:假设一个固定大小为W的窗口,依次划过arr,返回每一次滑出状况的最大值.例如,arr = [4,3,5,4,3,3,6,7], W = 3.返回:[5,5,5,4,6,7]. 福 ...

  3. Burpsuite抓包工具的使用

    一.打开工具 1处箭头为 代理127.0.0.1 端口8080 2处箭头为 证书 将证书ca下载到桌面上 选择第一个 选择下载到桌面即可 可以修改其后缀为der 此即为证书文件 此处使用火狐浏览器为示 ...

  4. C盘清理,移动node 依赖和缓存文件

    由于先前安装的node 没有做任何配置,都是傻瓜式下一步,导致了我很多依赖都放置C盘,内存占用过多:也不太好管理所有觉得将它移动到node安装目录 一.新建文件夹 在原本安装的nodejs目录下新建 ...

  5. Pycharm激活码,Pycharm稳定专属激活码(持续更新)

    分享一下 PyCharm 2023.1.2 最新激活注册码,破解教程如下,可免费永久激活,亲测有效,下面是详细文档哦~ 申明:本教程 PyCharm 激活码收集于网络,请勿商用,仅供个人学习使用,如有 ...

  6. Java的运算符和表达式(基础语法学习)

    一.运算符 ​ 在Java中用于程序计算的操作符统称为运算符,运算符分为如下几类 1.算数运算符 运算符 说明 + 加号两边是数值,可以运算,如果一边存在字符串,则当作连接符 a+b - 两个数相加, ...

  7. Google Code Prettify 代码高亮插件使用小结

    Google Code Prettify 是 Google 的一款代码高亮插件,它由 js 代码和 css 代码构成,用来高亮显示 HTML 页面中的源代码. Google Code Prettify ...

  8. C++面试八股文:static和const的关键字有哪些用法?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第7面: 面试官:C++中,static和const的关键字有哪些用法? 二师兄:satic关键字主要用在以下三个方面:1.用在全局作用域,修饰的变量 ...

  9. 聊聊Cola-StateMachine轻量级状态机的实现

    背景 在分析Seata的saga模式实现时,实在是被其复杂的 json 状态语言定义文件劝退,我是有点没想明白为啥要用这么来实现状态机:盲猜可能是基于可视化的状态机设计器来定制化流程,更方便快捷且上手 ...

  10. 【后端面经】MySQL主键、唯一索引、联合索引的区别和作用

    目录 0. 简介 1. 主键 2. 唯一索引 3. 联合索引 4. 索引对数据库操作的影响 5. 其他索引 5.1 普通索引 5.2 全文索引 5.3 前缀索引 6. 总结 7. 参考资料 0. 简介 ...