多线程并发场景中,时常需要线程协同,故而需要对当前线程进行阻塞,并唤醒需要协同的线程来一起完成任务。
通常处理方式有三种:
使用Object类下所提供的方法:
使用JUC中LockSupport阻塞工具类的方法:
LockSupport来自于JDK1.5,位于JUC包的locks子包,是一个非常方便实用的线程阻塞工具类,它定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,可以在线程内任意位置让线程阻塞、唤醒。
在AQS框架的源码中,当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具来完成。LockSupport 和 CAS 是Java并发包中并发工具(锁和其他同步类)控制机制的实现基础,而这两个基础其实又是依赖Unsafe类,然而Unsafe只是维护了一系列本地方法接口,因此真正的实现是在HotSpot的源码中,而HotSpot是采用C++来实现的!
AQS框架是JUC中实现同步组件的基石,而LockSupport可以说是AQS框架的基石之一。
1、LockSupport是非重入的。因为调用一次park方法,线程就被阻塞了。
**2、LockSupport的park阻塞、unpark唤醒的调用不需要任何条件对象,也而不需要先获取什么锁。**也就是说LockSupport只与线程绑定,就可以降低代码耦合性。
**3、park阻塞与unpark唤醒的调用顺序可以颠倒,不会出现死锁,并且可以重复多次调用unpark。**但是ThreadGroup类的stop和resume方法如果顺序反了,就会出现死锁现象。
**4、park支持中断唤醒,但是不会抛出InterruptedException异常。**意思就是使用Thread.interrupted() 可以使线程中断,会使park失效。
每个线程都与一个许可(permit)关联。unpark函数为线程提供permit,线程调用park函数则等待并消耗permit。permit默认是0,调用一次unpark就变成1,再调用一次park会消费permit,也就是将1变成0,park会立即返回。
如果原来没有permit,那么调用park会将相关线程阻塞在调用处,等待一个permit,这时调用unpark又会把permit置为1,使得阻塞的线程被唤醒。每个线程都有自己的permit,但是permit最多持有一个,重复调用unpark也不会积累。
LockSupport.park和LockSupport.unpark不会引发的死锁问题,因为由于许可的存在,即使unpark发生在park之前,也可以使得下一次的park操作立即返回。
和Object.wait相比,LockSupport.park不需要先获得某个对象的锁,也不会抛出InterruptedException 异常。
和synchronized相比,LockSupport.park()阻塞的线程可以被中断阻塞,但是不会抛出异常,并且中断之后不会清除中断标志位。被park阻塞的线程处于WAITING状态,超时park阻塞的线程则处于TIMED_WAITING状态。
LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark方法来唤醒一个被阻塞的线程。
除非许可证permit可用,否则出于线程调度目的禁用当前线程。 如果许可证可用,则该许可证被消耗,呼叫立即返回;否则,出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下三种情况之一:
此方法不报告导致方法返回的原因。调用方应首先重新检查导致线程停止的条件。调用方还可以在返回时确定线程的中断状态。
使给定线程的许可证permit可用(如果尚未可用)。如果线程在 park上被阻塞,那么它将解除阻塞。否则,它对 park 的下一次呼叫保证不会被阻塞。如果给定的线程尚未启动,则不能保证此操作有任何效果。
在 LockSupport.park()的底层主要是调用 Unsafa 类的 park方法,每个线程对象都有一个 Parker 实例,这个实例内部有提供park和unpark方法。
Unsafe这个类中有许多的native方法,通过字段偏移量(类似于C的指针),提供了Java语言与底层系统进行交互的接口,通过Unsafe可以直接操作底层系统,它具有直接内存管理、线程阻塞&唤醒的支持、CAS操作的支持、直接操作类、对象、变量等强大的功能。
JavaThread内部具有一个threadOb属性,这个属性实际上就是保存这着Java层面的一个Thread对象,而JavaThread继承了Thread,继承了_osthread字段。那么一个JavaThread对象和一个OSThread对象对应,同时又和一个Thread对象对应,这样它们三个的就被联系起来了。因此实际上一个Java的Thread对应着一个OS线程。
Unsafe可以直接操作JVM和底层系统,因此,可以通过Thread是直接找到JavaThread实例进行操作,因此即使我们在Thread中没有找到“permit”,但是这个“permit”肯定是在Hotspot的源码中能就见到!
JavaThread内部还有一个Parker类型的_parker属性,这个Parker实际上就是用来实现Java中的LockSupport 的park 和unpark的,即实现单个线程的阻塞和唤醒,也就是JUC的中线程阻塞、唤醒在JVM层面的实现。
Parker有一个_counter字段,这个字段实际上就是我们常说的“许可”,并且默认初始化为0。我们调用的park、unpark方法,实际上是调用的Parker的同名方法。
Parker源码中表示Parker继承了PlatformParker,由于Hotspot虚拟机为跨平台,针对不同操作系统有不同的实现,最常见的就是linux系统,以此来分析。
PlatformParker内部具有POSIX库标准的互斥量(锁)mutex和条件变量condition,那么实际上Parker的对于park和unpark的实现实际上就是用这两个工具实现的。
另外,PlatformParker还有一个cur_index属性,它的值为-1、0或者1,-1时初始化的值,调用park并返回的线程也会设置值为-1。如果不是-1,那么表示对应的parker中的条件变量上有线程被挂起,_cur_index等于0表示调用park相对时间的线程在第一个条件变量上被挂起,等于1则表示调用park绝对时间的线程在第二个条件变量上被挂起。
实际上mutex与condition都是posix标准的用于底层系统线程实现线程同步的工具。 mutex被称为互斥量锁,类似于Java的锁,即用来保证线程安全,一次只有一个线程能够获取到互斥量mutex,获取不到的线程则可能会阻塞。而这个condition可以类比于java的Condition,被称为条件变量,用于将不满足条件的线程挂起在指定的条件变量上,而当条件满足的时候,再唤醒对应的线程让其执行。
可以理解为,mutex与condition来实现加锁、释放锁。如同synchronized与监视器对象一样。
ParkEvent是用于Java级别的Synchronized关键字,Parker是JSR166来的并发工具集合,后面会统一使用ParkEvent。ParkerEvent 继承了PlatformEvent,基类PlatformEvent是特定于平台的,而ParkEvent则是平台无关的。而Parker 继承自PlatformParker。
1、判断是否需要阻塞等待,如果已经是 _counter >0, 不需要等待,将 _counter = 0 , 返回
2、如果 1 不成立,构造当前线程的 ThreadBlockInVM ,检查 _counter > 0 是否成立,成立则将 _counter 设置为 0, unlock mutex 返回;
3、如果 2 不成立,更具需要时间进行不同的函数等待,如果等待正确返回,则将 _counter 设置为0, unlock mutex , park 调用成功。
parker 类的定义如下
- Parker 类继承 os::PlatformParker。
- 有一个 _counter 属性,可以理解为是否可以调用 park 方法的许可证,只有 _count > 0 的时候才能调用;
- 提供了公开的 park 和 unpark 方法
LockSupport是JDK1.5时提供的用于实现单个线程等待、唤醒机制的阻塞工具,也是AQS框架的基石之一,另外两个则是CAS操作、volatile关键字。
每个Java线程内部,其对应着OS线程,而对线程的操作,也就是对于操作系统的操作。JDK提供Unsafe类可以直接操作底层系统,它本身具有直接内存管理、线程阻塞&唤醒的支持、CAS操作的支持、直接操作类、对象、变量等强大的功能。Unsafe类关于线程阻塞、唤醒,有提供Parker类,Parker类继承PlatformParker。
PlatformParker内部具有POSIX库标准的互斥量(锁)mutex和条件变量condition,那么实际上Parker的对于park和unpark的实现实际上就是用这两个工具实现的。
LockSupport.park方法实质上就是调用了Unsafe类的Native方法,执行LockSupport.park方法不会释放此前获取到的synchronized锁或者lock锁,因为LockSupport的方法根本就与我们常说的“锁”无关。其本质依赖了mutex和Condition工具。