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

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. Django-管理员用户的创建

    命令:python manage.py createsuperuser python manage.py createsuperuser Type 'manage.py help' for usage ...

  2. Redis数据结构三之压缩列表

    本文首发于公众号:Hunter后端 原文链接:Redis数据结构三之压缩列表 本篇笔记介绍压缩列表. 在 Redis 3.2 版本之前,压缩列表是列表对象.哈希对象.有序集合对象的的底层实现之一. 因 ...

  3. Pose泰裤辣! 一键提取姿态生成新图像

    摘要:从图像提取人体姿态,用姿态信息控制生成具有相同姿态的新图像. 本文分享自华为云社区<Pose泰裤辣! 一键提取姿态生成新图像>,作者: Emma_Liu . 人体姿态骨架生成图像 C ...

  4. 在开发过程中使用git rebase还是git merge,优缺点分别是什么?

    前言 在开发过程中,git rebase 和 git merge 都是常见的代码版本管理工具.它们都能够将分支合并到主分支,并且都有各自的优缺点. git merge git merge 是一种将两个 ...

  5. 【源码解读】asp.net core源码启动流程精细解读

    引言 core出来至今,已经7年了,我接触也已经4年了,从开始的2.1,2.2,3.1,5,6再到如今的7,一直都有再用,虽然我是一个Winform仔,但是源码一直从3.1到7都有再看,然后在QQ上面 ...

  6. qq飞车端游最全按键指法教学

    目录 起步篇 超级起步 弹射起步 段位起步 基础篇 点飘 撞墙漂移 撞墙点喷 进阶篇 双喷 叠喷 断位漂移 段位双喷 侧身漂移 快速出弯 CW WCW CWW 牵引 甩尾点飘 甩尾漂移 右侧卡 左侧卡 ...

  7. 【QCustomPlot】性能提升之修改源码(版本 V2.x.x)

    说明 使用 QCustomPlot 绘图库的过程中,有时候觉得原生的功能不太够用,比如它没有曲线平滑功能:有时候又觉得更新绘图数据时逐个赋值效率太低,如果能直接操作内存就好了:还有时候希望减轻 CPU ...

  8. 远程挂载 NFS 共享目录引发死机问题

    集群的存储空间有限,把一些历史的归档数据放在了公司的另外一台老旧存储服务器上,并使用 NFS 把它挂载到了 log 节点.周末的时候机房空调故障,旧存储服务器挂掉了!周一上班,在集群登陆节点使用df ...

  9. 【python基础】循环语句-while循环

    1.初识while循环 循环语句主要的作用是在多次处理具有相同逻辑的代码时使用.while循环是Python提供的循环语句之一. while循环的语法格式之一: 比如我们输出1-10之间的奇数,编写程 ...

  10. java接口返回图片链接或pdf链接如何设置在线预览还是下载

    之前文章说到了如何通过将文件转成图片或者pdf来实现在线预览,一般来说在线预览图片或者pdf都是存储在图片服务器上的,在通过接口调用把文件返回给前端,但是把文件返回给前端效果一般是有两种:在线预览和下 ...