自己动手写把”锁”---LockSupport介绍
本篇是《自己动手写把"锁"》系列技术铺垫的最后一个知识点。本篇主要讲解LockSupport工具类,它用来实现线程的挂起和唤醒。
LockSupport是Java6引入的一个工具类,它简单灵活,应用广泛。
一、简单
俗话说,没有比较就没有伤害。这里咱们还是通过对比来介绍LockSupport的简单。
在没有LockSupport之前,线程的挂起和唤醒咱们都是通过Object的wait和notify/notifyAll方法实现。
写一段例子代码,线程A执行一段业务逻辑后调用wait阻塞住自己。主线程调用notify方法唤醒线程A,线程A然后打印自己执行的结果。
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
obj.wait();
}catch (Exception e){
e.printStackTrace();
}
System.out.println(sum);
}
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
obj.notify();
}
}
执行这段代码,不难发现这个错误:
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
原因很简单,wait和notify/notifyAll方法只能在同步代码块里用(这个有的面试官也会考察)。所以将代码修改为如下就可正常运行了:
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
synchronized (obj){
obj.wait();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println(sum);
}
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
synchronized (obj){
obj.notify();
}
}
}
那如果咱们换成LockSupport呢?简单得很,看代码:
public class TestObjWait {
public static void main(String[] args)throws Exception {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
LockSupport.park();
System.out.println(sum);
}
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
LockSupport.unpark(A);
}
}
直接调用就可以了,没有说非得在同步代码块里才能用。简单吧。
二、灵活
如果只是LockSupport在使用起来比Object的wait/notify简单,那还真没必要专门讲解下LockSupport。最主要的是灵活性。
上边的例子代码中,主线程调用了Thread.sleep(1000)方法来等待线程A计算完成进入wait状态。如果去掉Thread.sleep()调用,代码如下:
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
try {
synchronized (obj){
obj.wait();
}
}catch (Exception e){
e.printStackTrace();
}
System.out.println(sum);
}
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
//Thread.sleep(1000);
synchronized (obj){
obj.notify();
}
}
}
多运行几次上边的代码,有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。原因就在于:主线程调用完notify后,线程A才进入wait方法,导致线程A一直阻塞住。由于线程A不是后台线程,所以整个程序无法退出。
那如果换做LockSupport呢?LockSupport就支持主线程先调用unpark后,线程A再调用park而不被阻塞吗?是的,没错。代码如下:
public class TestObjWait {
public static void main(String[] args)throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
LockSupport.park();
System.out.println(sum);
}
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
//Thread.sleep(1000);
LockSupport.unpark(A);
}
}
不管你执行多少次,这段代码都能正常打印结果并退出。这就是LockSupport最大的灵活所在。
总结一下,LockSupport比Object的wait/notify有两大优势:
①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
三、应用广泛
LockSupport在Java的工具类用应用很广泛,咱们这里找几个例子感受感受。以Java里最常用的类ThreadPoolExecutor为例。先看如下代码:
public class TestObjWait {
public static void main(String[] args)throws Exception {
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(1000);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1000, TimeUnit.SECONDS,queue);
Future<String> future = poolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(5);
return "hello";
}
});
String result = future.get();
System.out.println(result);
}
}
代码中我们向线程池中扔了一个任务,然后调用Future的get方法,同步阻塞等待线程池的执行结果。
这里就要问了:get方法是如何组塞住当前线程?线程池执行完任务后又是如何唤醒线程的呢?
咱们跟着源码一步步分析,先看线程池的submit方法的实现:

在submit方法里,线程池将我们提交的基于Callable实现的任务,封装为基于RunnableFuture实现的任务,然后将任务提交到线程池执行,并向当前线程返回RunnableFutrue。
进入newTaskFor方法,就一句话:return new FutureTask<T>(callable);
所以,咱们主线程调用future的get方法就是FutureTask的get方法,线程池执行的任务对象也是FutureTask的实例。
接下来看看FutureTask的get方法的实现:

比较简单,就是判断下当前任务是否执行完毕,如果执行完毕直接返回任务结果,否则进入awaitDone方法阻塞等待。

awaitDone方法里,首先会用到上节讲到的cas操作,将线程封装为WaitNode,保持下来,以供后续唤醒线程时用。再就是调用了LockSupport的park/parkNanos组塞住当前线程。
上边已经说完了阻塞等待任务结果的逻辑,接下来再看看线程池执行完任务,唤醒等待线程的逻辑实现。
前边说了,咱们提交的基于Callable实现的任务,已经被封装为FutureTask任务提交给了线程池执行,任务的执行就是FutureTask的run方法执行。如下是FutureTask的run方法:

c.call()就是执行我们提交的任务,任务执行完后调用了set方法,进入set方法发现set方法调用了finishCompletion方法,想必唤醒线程的工作就在这里边了,看看代码实现吧:

没错就在这里边,先是通过cas操作将所有等待的线程拿出来,然后便使用LockSupport的unpark唤醒每个线程。
在使用线程池的过程中,不知道你有没有这么一个疑问:线程池里没有任务时,线程池里的线程在干嘛呢?
看过我的这篇文章《线程池的工作原理与源码解读》的读者一定知道,线程会调用队列的take方法阻塞等待新任务。那队列的take方法是不是也跟Future的get方法实现一样呢?咱们来看看源码实现。
以ArrayBlockingQueue为例,take方法实现如下:

与想象的有点出入,他是使用了Lock的Condition的await方法实现线程阻塞。但当我们继续追下去进入await方法,发现还是使用了LockSupport:

限于篇幅,jdk里的更多应用就不再追下去了。
四、LockSupport的实现
学习要知其然,还要知其所以然。接下来不妨看看LockSupport的实现。
进入LockSupport的park方法,可以发现它是调用了Unsafe的park方法,这是一个本地native方法,只能通过openjdk的源码看看其本地实现了。

它调用了线程的Parker类型对象的park方法,如下是Parker类的定义:

类中定义了一个int类型的_counter变量,咱们上文中讲灵活性的那一节说,可以先执行unpark后执行park,就是通过这个变量实现,看park方法的实现代码(由于方法比较长就不整体截图了):

park方法会调用Atomic::xchg方法,这个方法会原子性的将_counter赋值为0,并返回赋值前的值。如果调用park方法前,_counter大于0,则说明之前调用过unpark方法,所以park方法直接返回。
接着往下看:

实际上Parker类用Posix的mutex,condition来实现的阻塞唤醒。如果对mutex和condition不熟,可以简单理解为mutex就是Java里的synchronized,condition就是Object里的wait/notify操作。
park方法里调用pthread_mutex_trylock方法,就相当于Java线程进入Java的同步代码块,然后再次判断_counter是否大于零,如果大于零则将_counter设置为零。最后调用pthread_mutex_unlock解锁,相当于Java执行完退出同步代码块。如果_counter不大于零,则继续往下执行pthread_cond_wait方法,实现当前线程的阻塞。
最后再看看unpark方法的实现吧,这块就简单多了,直接上代码:

图中的1和4就相当于Java的进入synchronized和退出synchronized的加锁解锁操作,代码2将_counter设置为1,同时判断先前_counter的值是否小于1,即这段代码:if(s<1)。如果不小于1,则就不会有线程被park,所以方法直接执行完毕,否则就会执行代码3,来唤醒被阻塞的线程。
通过阅读LockSupport的本地实现,我们不难发现这么个问题:多次调用unpark方法和调用一次unpark方法效果一样,因为都是直接将_counter赋值为1,而不是加1。简单说就是:线程A连续调用两次LockSupport.unpark(B)方法唤醒线程B,然后线程B调用两次LockSupport.park()方法, 线程B依旧会被阻塞。因为两次unpark调用效果跟一次调用一样,只能让线程B的第一次调用park方法不被阻塞,第二次调用依旧会阻塞。
到这里,自己实现一把“锁”用到的技术点都已经介绍完了,甚至本节还介绍了锁的具体实现,相信即使没有最后一篇介绍怎么实现“锁”,大家也能动手写个锁了。
如果没有看过之前的文章,可以先看之前这几篇:
自己动手写把”锁”之---锁的作用
自己动手写把”锁”之---JMM和volatile
自己动手写把”锁”---原子性操作
----------------------------------------------
有兴趣的朋友,可以加入我的知识圈,一起研究讨论。
我正在「JAVA互联网技术」和朋友们讨论有趣的话题,你一起来吧?
自己动手写把”锁”---LockSupport介绍的更多相关文章
- 自己动手写把”锁”---LockSupport深入浅出
本篇是<自己动手写把"锁">系列技术铺垫的最后一个知识点.本篇主要讲解LockSupport工具类,它用来实现线程的挂起和唤醒. LockSupport是Java6引入 ...
- 死磕 java同步系列之自己动手写一个锁Lock
问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...
- 自己动手写java锁
1.LockSupport的park和unpark方法的基本使用,以及对线程中断的响应性 LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语.java锁和同步器 ...
- 自己动手写把”锁”之---JMM和volatile
一.JAVA内存模型 关于Java内存模型的文章,网上真的数不胜数.在这里我就不打算说的很详细.很严谨了.只力求大家能更好的理解和运用,为后边的技术点做铺垫. 内存模型并不是Java独有的概念,而 ...
- 60行自己动手写LockSupport是什么体验?
60行自己动手写LockSupport是什么体验? 前言 在JDK当中给我们提供的各种并发工具当中,比如ReentrantLock等等工具的内部实现,经常会使用到一个工具,这个工具就是LockSupp ...
- 自己动手写处理器之第一阶段(3)——MIPS32指令集架构简单介绍
将陆续上传本人写的新书<自己动手写处理器>(尚未出版).今天是第四篇.我尽量每周四篇 1.4 MIPS32指令集架构简单介绍 本书设计的处理器遵循MIPS32 Release 1架构,所以 ...
- 自己动手写处理器之第二阶段(2)——Verilog HDL简单介绍
将陆续上传本人写的新书<自己动手写处理器>(尚未出版),今天是第六篇.我尽量每周四篇 2.3 Verilog HDL简单介绍 本书实现的OpenMIPS处理器是使用Verilog HDL编 ...
- Java并发编程:自己动手写一把可重入锁
关于线程安全的例子,我前面的文章Java并发编程:线程安全和ThreadLocal里面提到了,简而言之就是多个线程在同时访问或修改公共资源的时候,由于不同线程抢占公共资源而导致的结果不确定性,就是在并 ...
- AQS学习(一)自旋锁原理介绍(为什么AQS底层使用自旋锁队列?)
1.什么是自旋锁? 自旋锁作为锁的一种,和互斥锁一样也是为了在并发环境下保护共享资源的一种锁机制.在任意时刻,只有一个执行单元能够获得锁. 互斥锁通常利用操作系统提供的线程阻塞/唤醒机制实现,在争用锁 ...
随机推荐
- Git快速入门和常用命令
一.快速入门 本地初始化一个项目 首先,你需要执行下面两条命令,作为 git 的基础配置,作用是告诉 git 你是谁,你输入的信息将出现在你创建的提交中. git config --global us ...
- 异步编程Async/await关键字
异步编程Async \await 关键字在各编程语言中的发展(出现)纪实. 时间 语言版本 2012.08.15 C#5.0(VS2012) 2015.09.13 Python 3.5 2016.03 ...
- Python爬虫(十五)_案例:使用bs4的爬虫
本章将从Python案例讲起:所使用bs4做一个简单的爬虫案例,更多内容请参考:Python学习指南 案例:使用BeautifulSoup的爬虫 我们已腾讯社招页面来做演示:http://hr.ten ...
- 玩了几天的ARToolKit
因为玩的不深,就说一说配置环境出错的解决. 假如大家网上下载的ARToolKit下面有glut32.dll,那按照教程来配置环境的时候(其中有一步是:将OpenGL的dll拷贝到windows sys ...
- Oracle 11gR2 用exp无法导出空表解决方法
Oracle 11gR2 用exp无法导出空表解决方法 在11gR2中有个新特性,当表无数据时,不分配segment以节省空间.Oracle 当然在运行export导出时,空表则无法导出,可是还是有解 ...
- 解决 PclZip 中文乱码问题
在使用 Pclzip 时出现无法压缩/解压文件的现象,追踪错误信息发现无法打开文件/文件夹.可是文件夹权限正确,打印文件路径之后发现是乱码. 出现这个问题的解决办法是windows下zip内的 ...
- MVC Anti-XSS方案
1:Form提交模式 在使用Form提交时,MVC框架提供了一个默认的机制.如果数据中含有恶意字,则会自动转向出错页面. 2:Ajax+JSON提交模式. MVC框架未提供对于Json数据的Ant ...
- Winform开发框架中工作流模块的表设计分析
在较早博客随笔里面写过文章<Winform开发框架之简易工作流设计>之后,很久没有对工作流部分进行详细的介绍了,本篇继续这个主题,详细介绍其中的设计.实现及效果给大家,这个工作流在好几年前 ...
- 传统controller与controllerAs
传统controller与controllerAs(前面为传统,后面为controllerAs,分割线分隔): 路由模块: .state('home.packing', { url: '/packin ...
- git命令的使用
git命令行的使用 0. 工作中常使用的命令行(小结) 假设我们工作共同使用的开发分支为dev,我自己的开发分支为dev_cx.安装git,在工作文件夹下打开git bash. $ git check ...