前言:
此刻咱们对“java怎么唤醒一个阻塞的线程”大体比较关怀,朋友们都需要了解一些“java怎么唤醒一个阻塞的线程”的相关知识。那么小编也在网络上收集了一些关于“java怎么唤醒一个阻塞的线程””的相关知识,希望姐妹们能喜欢,你们一起来学习一下吧!难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境JDK v11IntelliJIDEA v2018.3友情提示本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!1.温故知新
前面在《“全栈2019”Java多线程第四十五章:查询Condition上的等待线程》一章中介绍了如何查询Condition上是否有线程正在等待和有多少线程正在等待。用锁的hasWaiters(Condition condition)方法查询Condition上是否有线程正在等待,再用getWaitQueueLength(Condition condition)方法获取Condition上等待的线程的个数。
在《“全栈2019”Java多线程第四十六章:判断任意线程是否已持有写锁》一章中介绍了如何判断任意线程是否已持有写锁。通过ReentrantReadWriteLock的isWriteLocked()方法来判断写锁是否已被任意线程持有。
在《“全栈2019”Java多线程第四十七章:判断锁是否为公平锁isFair()》一章中介绍了如何判断锁是否为公平锁isFair()方法。
在《“全栈2019”Java多线程第四十七章:判断锁是否为公平锁isFair()》一章中介绍了如何判断锁是否为公平锁isFair()方法。
在《“全栈2019”Java多线程第四十八章:读写锁实战高并发容器》一章中介绍了如何使用读写锁实战简易版高并发容器。
现在介绍LockSupport的阻塞线程park()方法和唤醒线程unpark()方法。
2.抛砖引玉
通过一个例子来引出LockSupport。
这个例子很简单,就是一个线程被等待,然后让主线程去唤醒它。
下面我们就开始来准备这个例子,先准备隐式锁synchronized版本的,再准备显式锁Lock版本,最后再是LockSupport版本。
首先,创建出一个线程并重写run()方法:
在run()方法里面写上同步代码块(当然了,你也可以使用显式锁Lock),目的是让当前线程等待:
接着,在同步代码块中调用wait()方法使当前线程等待:
然后,在线程被等待前后各输出一句话,目的就是为了看清楚线程什么时候被等待和被唤醒:
好了,run()方法书写完毕。
然后,启动线程:
因为执行唤醒操作需要锁,所以在主线程中也写上同步代码块:
然后,在同步代码块中调用notifyAll()方法去唤醒被等待的线程:
最后,为了让thread线程一定被等待,我们在主线程调用notifyAll()方法之前睡1秒钟:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。隐式锁synchronized版本没什么问题。
下面用显式锁Lock改写该例子。
先创建出显式锁Lock:
然后,将thread线程里面的隐式锁synchronized换成显式锁Lock:
接着,将主线程里面的隐式锁synchronized换成显式锁Lock:
因为要用到等待/唤醒,所以需要Condition对象:
接下来,用Condition的await()方法替换wait()方法:
接着,用Condition的signalAll()方法替换notifyAll()方法:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。显式锁Lock版本也没有问题。
最后,我们就用LockSupport来改写该例子。
首先,将显式锁Lock和Condition对象移除掉:
其次,将thread线程里面的获取/释放锁代码块移除掉:
接着,将主线程里面的获取/释放锁代码块移除掉:
前面该移除的代码移除之后的样子:
将condition.await()方法替换为LockSupport.park()方法:
因为LockSupport.park()方法不抛出异常,所以try-catch代码块移除掉:
接下来,将condition.signalAll()方法替换为LockSupport.unpark()方法:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。LockSupport版本也没问题。
无论是隐式锁synchronized,还是显式锁Lock,还是LockSupport,它们都可以让线程被等待和被唤醒。
那它们三个有什么区别呢?
从代码层面上看,隐式锁synchronized和显式锁Lock都需要同步域,而LockSupport不用,它不需要同步域就可以使用。
还有一点,LockSupport不在乎执行等待操作的线程和执行唤醒操作的线程它们两个谁先执行,谁先执行结果都一样。
什么意思?
还是拿例子来解释说明吧。
还是刚刚的例子,只不过我们把LockSupport.unpark()方法放在启动线程之后:
顺便把使主线程睡1秒钟的代码移至thread线程里面去:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
程序经过这样改写之后,thread线程和主线程都有可能先执行,而无论它们谁先执行结果都一样。
如果是隐式锁的wait()方法、notifyAll()方法或显式锁的Condition对象的await()方法、signalAll()方法的话就不行,当执行唤醒操作的线程先运行,执行等待操作的线程后运行时,结果一定是线程被永久等待。
但LockSupport不同的是,即使执行唤醒操作的线程先执行,执行等待操作的线程后执行,结果也还是等待的线程被唤醒。
下面我们就看看什么是LockSupport。
3.什么是LockSupport?
LockSupport是一个阻塞或唤醒线程的类。
从此行代码来看,LockSupport无法创建对象,俗称单例。
它里面有很多阻塞或唤醒线程的方法,都是静态的(因为LockSupport无法创建对象):
static Object getBlocker(Thread t)static void park()static void park(Object blocker)static void parkNanos(long nanos)static void parkNanos(Object blocker, long nanos)static void parkUntil(long deadline)static void parkUntil(Object blocker, long deadline)static void unpark(Thread thread)
本章内容只介绍park()方法和unpark()方法。
4.阻塞当前线程park()方法
当我们需要阻塞一个线程时,可以调用LockSupport的park()方法。
park()方法在LockSupport类中的源码:
将注释翻译成中文:
中文注释全文:
除非有唤醒标识,否则阻塞当前线程。被阻塞的线程可以被unpark(Thread thread)方法唤醒或中断。
去掉注释版:
park()方法作用是除非有唤醒标识,否则阻塞当前线程。被阻塞的线程可以被unpark(Thread thread)方法唤醒或中断。
访问权限
public:park()方法是公开的。
static:park()方法是静态的。
void:park()方法无返回值。
park()方法只能被类调用。
参数
无。
抛出的异常
无。
应用
在上一小节中,我们也是用过park()方法,这里再重新用一遍。
首先,创建线程并重写run()方法:
然后,在run()方法里面使用LockSupport.park()方法使其当前线程等待:
接着,在等待前后各输出一句话,目的就是为了看清楚线程何时被等待以及何时被唤醒:
run()方法书写完毕。
启动,启动线程:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。程序一直停着不动,因为前台thread线程被等待,没有被中断或唤醒的情况下它不会醒来。
下面,我们就来看看中断线程会怎样。
5.中断线程
还是上一小节的例子,只不过我们在启动线程后面加上中断线程的代码:
在中断线程之前,我们先让主线程睡1秒钟:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。等待的线程被中断后醒来继续往下执行。
我们还可以通过LockSupport类的unpark(Thread thread)方法来唤醒被等待的线程。
下面,我们就来看看unpark(Thread thread)方法。
6.唤醒被等待的线程unpark(Thread thread)方法
unpark(Thread thread)方法在LockSupport类中的源码:
将注释翻译成中文:
中文注释全文:
唤醒指定线程,若该线程已启动,则为其提供唤醒标识,保证下一次被等待时将自动被唤醒。
去掉注释版:
unpark(Thread thread)方法作用是唤醒指定线程,若该线程已启动,则为其提供唤醒标识,保证下一次被等待时将自动被唤醒。
访问权限
public:unpark(Thread thread)方法是公开的。
static:unpark(Thread thread)方法是静态的。
void:unpark(Thread thread)方法无返回值。
unpark(Thread thread)方法只能被类调用。
参数
无。
抛出的异常
thread:指定要唤醒的线程。
应用
下面,我们就来试一试unpark(Thread thread)方法。
还是上一小节的例子,将中断线程代码替换为LockSupport.unpark(Thread thread)方法:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。等待的线程被唤醒。
7.unpark(Thread thread)方法的豁免权
前面也说过,LockSupport不同的是,即使执行唤醒操作的线程先执行,执行等待操作的线程后执行,结果也还是等待的线程被唤醒。
park()方法和unpark(Thread thread)方法中的唤醒标识(许可)是什么?
是一个计数器,默认为0,如果先执行的是unpark(Thread thread)方法,那么计数器就为1,当执行到park()方法时,判断计数器是否大于0,若计数器大于0,那么线程立即就被唤醒;否则线程继续等待。
这就是为什么即使先执行LockSupport.unpark(Thread thread)方法后执行LockSupport.park()方法也能唤醒线程的原因。相当于线程拿到了一次豁免权。
按照调用一次unpark(Thread thread)方法计数器就为1的说法,那么我调用很多次unpark(Thread thread)方法,那岂不是可以有很多豁免权?
不不不,千万别这样想,多次调用unpark(Thread thread)方法时,计数器只为1,可以理解为计数器=1,而非计数器++。
通过实际代码来验证。
还是之前的例子,这里调用两次unpark(Thread thread)方法:
再在thread线程里面调用两次park()方法,在两个方法调用之前各输出一句话,目的是为了看看如何被阻塞的:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。即使我们调用了两次unpark(Thread thread)方法,也没能让线程两度从阻塞中醒来,程序还是停着不动。不过这也说明了多次调用unpark(Thread thread)方法只会给一次唤醒标识(许可)。
8.未启动的线程没有豁免权
最后说一点unpark(Thread thread)方法需要注意的地方:
unpark(Thread thread)方法传入的线程需要是启动状态才可以设置唤醒标识。
启动状态?
就是调用了start()方法的线程。
这里通过例子来验证。
还是上一小节的例子,先将多次调用park()方法和unpark(Thread thread)方法的代码移除:
然后,将unpark(Thread thread)方法移至启动线程代码之前:
最后,移除使当前线程睡1秒钟的代码:
移除之后的代码:
例子改写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。这次线程并没有被唤醒,原因在于unpark(Thread thread)方法传入的线程未启动,得不到唤醒标识(许可),后面执行park()方法时,没有豁免权,直接被阻塞。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:
总结LockSupport是一个阻塞或唤醒线程的类。LockSupport无法创建对象,俗称单例。它里面有很多阻塞或唤醒线程的方法,都是静态的(因为LockSupport无法创建对象)。park()方法作用是除非有唤醒标识,否则阻塞当前线程。被阻塞的线程可以被unpark(Thread thread)方法唤醒或中断。unpark(Thread thread)方法作用是唤醒指定线程,若该线程已启动,则为其提供唤醒标识,保证下一次被等待时将自动被唤醒。
至此,Java中LockSupport中的park()方法和unpark()方法相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java多线程第四十八章:读写锁实战高并发容器
下一章
“全栈2019”Java多线程第五十章:设置/获取LockSupport同步对象
学习小组
加入同步学习小组,共同交流与进步。
方式一:关注头条号Gorhaf,私信“Java学习小组”。方式二:关注公众号Gorhaf,回复“Java学习小组”。全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!
标签: #java怎么唤醒一个阻塞的线程