失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Java LockSupport以及park unpark方法源码深度解析

Java LockSupport以及park unpark方法源码深度解析

时间:2020-05-03 06:11:03

相关推荐

Java LockSupport以及park unpark方法源码深度解析

介绍了JUC中的LockSupport阻塞工具以及park、unpark方法的底层原理,从Java层面深入至JVM层面。

文章目录

1 LockSupport的概述2 LockSupport的特征和原理2.1 特征2.2 原理3 LockSupport的方法解析与测试3.1 基本方法3.2 JDK1.6的新方法3.3 测试3.3.1 park/unpark 基本测试3.3.2 Park 线程状态测试3.3.3 Park 中断测试3.3.4 park broker测试4 LockSupport的底层实现原理4.1 Unsafe4.2 Thread4.3 Parker4.4 PlatformParker4.5 mutex与condition概述4.6 park方法4.6.1 虚假唤醒(spurious wakeup)4.7 unpark方法5 LockSupport的总结

1 LockSupport的概述

public class LockSupport

extends Object

LockSupport来自于JDK1.5,位于JUC包的locks子包,是一个非常方便实用的线程阻塞工具类,它定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,可以在线程内任意位置让线程阻塞、唤醒。

在AQS框架的源码中,当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具来完成。LockSupport 和 CAS 是Java并发包中并发工具(锁和其他同步类)控制机制的实现基础,而这两个基础其实又是依赖Unsafe类,然而Unsafe只是维护了一系列本地方法接口,因此真正的实现是在HotSpot的源码中,而HotSpot是采用C++来实现的!

本文先讲解LockSupport的大概原理以及Java代码的实现,最后介绍Hotspot底层的实现。AQS框架是JUC中实现同步组件的基石,而LockSupport可以说是AQS框架的基石之一。

2 LockSupport的特征和原理

2.1 特征

LockSupport是非重入的,这个很简单,因为park的意思仅仅是阻塞某个线程而已,并不是“锁”,调用一次park方法,线程就被阻塞了。LockSupport的park阻塞、unpark唤醒的调用不需要任何条件对象,也而不需要先获取什么锁。在一定程度上降低代码的耦合度,即LockSupport只与线程绑定,并且被park的线程并不会释放之前获取到的锁。park阻塞与unpark唤醒的调用顺序可以颠倒,不会出现死锁,并且可以重复多次调用unpark;而stop和resume方法如果顺序反了,就会出现死锁现象。park支持中断唤醒,但是不会抛出InterruptedException异常,可以从isInterrupted不会清除中断标记)、interrupted(会清除中断标记)方法中获得中断标记。

2.2 原理

每个线程都与一个许可(permit)关联。unpark函数为线程提供permit,线程调用park函数则等待并消耗permit。

permit默认是0,调用一次unpark就变成1,调用一次park会消费permit,也就是将1变成0,park会立即返回。

如果原来没有permit,那么调用park会将相关线程阻塞在调用处等待一个permit,这时调用unpark又会把permit置为1,使得阻塞的线程被唤醒。

每个线程都有自己的permit,但是permit最多持有一个,重复调用unpark也不会积累。

和Thread.suspend和 Thread.resume相比, LockSupport.park和LockSupport.unpark不会引发的死锁问题(如果resume在suspend前执行,会导致线程无法继续执行发生死锁),因为由于许可的存在,即使unpark发生在park之前,它也可以使得下一次的park操作立即返回。

和Object.wait相比,LockSupport.park不需要先获得某个对象的锁,也不会抛出InterruptedException 异常。

和synchronized相比,LockSupport.park()阻塞的线程可以被中断阻塞,但是不会抛出异常,并且中断之后不会清除中断标志位。

被park阻塞的线程处于WAITING状态,超时park阻塞的线程则处于TIMED_WAITING状态。

以上只是非常简单易懂的原理,后面会有详细的解释!

3 LockSupport的方法解析与测试

3.1 基本方法

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark方法来唤醒一个被阻塞的线程。

/*** 尝试获取一个许可,如果没有则阻塞当前线程,响应中断;以下情况会返回* 1.调用unpark(Thread thread)获得许可,这个unpark操作可以在park之前或者之后,如果park之前已经获得了许可,则调用了park会发上返回* 2.当前线程被中断(interrupt()),返回时不会抛出异常* 3.因为虚假唤醒而返回*/public static void park() {UNSAFE.park(false, 0L);}/*** park()的扩展函数,时间是相对当前时间的时间段,单位为纳秒,如果超时自动返回** @param nanos 时间段纳秒*/public static void parkNanos(long nanos) {if (nanos > 0)UNSAFE.park(false, nanos);}/*** park()的扩展函数,时间是基于绝对时间(1970开始)的时间点,单位为毫秒,如果超时自动返回** @param deadline 时间点的毫秒值*/public static void parkUntil(long deadline) {UNSAFE.park(true, deadline);}/*** 提供一个许可,唤醒线程的方法就这一个。* 1.如果thread 之前没有持有许可,则让thread 线程持有一个,如果这前有许可了,那么数量不会增加* 2.如果thread 之前因调用park()而被挂起,则调用unpark()后,该线程会被唤醒。* 3.如果thread 之前没有调用park(),则调用unpark()方法后,后续再一次调用park()方法时,其会立刻返回。** @param thread*/public static void unpark(Thread thread) {if (thread != null)UNSAFE.unpark(thread);}

3.2 JDK1.6的新方法

在JDK1.5之前,当使用synchronized关键字使线程阻塞在一个监视器对象上时,通过线程dump能够查看到该线程的阻塞对象,方便问题定位,而JDK1.5推出LockSupport工具时却遗漏了这一点,因为LockSupport的方法不需要有监视器对象也不需要获得锁即可执行,致使在查看线程dump时无法提供阻塞对象的信息。

因此,在JDK1.6中,LockSupport新增了3个含有阻塞对象的park方法以及一个获取broker的方法,用以替代原有的park方法,方便问题定位。

/*** JDK1.6的新方法,除了参数之外其他和park()一样* 参数:blocker,用来标识当前线程在等待的对象,即记录线程被阻塞时被谁阻塞的,用于线程监控和分析工具来定位* 根据源码可以看到的是参数blocker是在park之前先通过setBlocker()记录阻塞线程的发起者object,当线程锁被释放后再次清除记录;* 推荐使用该方法,而不是park(),因为这个函数可以记录阻塞的发起者,如果发生死锁方便查看,在线程dump中会明确看到这个对象** @param blocker 与该线程关联的阻塞对象*/public static void park(Object blocker) {//获取当前线程Thread t = Thread.currentThread();//记录是哪个对象对该线程发起的阻塞操作setBlocker(t, blocker);//挂起线程UNSAFE.park(false, 0L);//执行到这一步,说明线程被唤醒了,此时清除brokersetBlocker(t, null);}/*** 和park(Object blocker)一样,增加了超时时间,单位为纳秒,超时立即返回,** @param blocker 与该线程关联的阻塞对象* @param nanos 超时时间段*/public static void parkNanos(Object blocker, long nanos) {if (nanos > 0) {Thread t = Thread.currentThread();setBlocker(t, blocker);UNSAFE.park(false, nanos);setBlocker(t, null);}}/*** 和park(Object blocker)一样,增加了超时时间点,单位为毫秒,超时立即返回** @param blocker 与该线程关联的阻塞对象* @param deadline 超时时间点*/public static void parkUntil(Object blocker, long deadline)/*** 查看与该线程关联的阻塞对象,如果没有设置blocker就会获取不到** @param t 制定线程* @return 阻塞对象*/public static Object getBlocker(Thread t) {if (t == null)throw new NullPointerException();return UNSAFE.getObjectVolatile(t, parkBlockerOffset);}/*** 设置broker的方法,该方法属于LockSupport的私有方法** @param t 当前线程* @param arg 要设置broker对象*/private static void setBlocker(java.lang.Thread t, Object arg) {// 内部同样调用UNSAFE的方法UNSAFE.putObject(t, parkBlockerOffset, arg);}/*** 在Thread线程定义中,具有一个parkBlocker属性,这个属性就是用来存放broker的属性*/public class Thread implements Runnable {volatile Object parkBlocker;//……}

3.3 测试

3.3.1 park/unpark 基本测试

/*** park/unpark测试*/@Testpublic void test2() {System.out.println("begin park");//调用park方法LockSupport.park();//使当前线程获取到许可证,明显执行不到这一步来,因为在上一步就已经阻塞了LockSupport.unpark(Thread.currentThread());System.out.println("end park");}/*** park/unpark测试*/@Testpublic void test3() {System.out.println("begin park");//使当前线程先获取到许可证LockSupport.unpark(Thread.currentThread());//再次调用park方法,先获得了许可,因此该方法不会阻塞LockSupport.park();System.out.println("end park");}/*** park/unpark测试*/@Testpublic void test4() {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {long currentTimeMillis = System.currentTimeMillis();System.out.println("begin park");LockSupport.park();System.out.println("end park");System.out.println(System.currentTimeMillis() - currentTimeMillis);}});thread.start();//开放或者注释该行代码,观察end park时间//Thread.sleep(2000);//使当子线程获取到许可证LockSupport.unpark(thread);}

3.3.2 Park 线程状态测试

/*** park线程状态测试** @throws InterruptedException*/@Testpublic void test1() throws InterruptedException {//park不限时Thread thread = new Thread(() -> LockSupport.park());//park限时Thread thread2 = new Thread(() -> LockSupport.parkNanos(3000000000l));thread.start();thread2.start();//主线睡眠一秒,让子线程充分运行Thread.sleep(1000);//获取处于park的子线程状态System.out.println(thread.getState()); System.out.println(thread2.getState());}

结果是

WAITING

TIMED_WAITING

3.3.3 Park 中断测试

/*** park中断测试*/@Testpublic void test5() throws InterruptedException {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {//最开始中断标志位位falseSystem.out.println(Thread.currentThread().isInterrupted());long currentTimeMillis = System.currentTimeMillis();System.out.println("begin park");LockSupport.park();System.out.println("end park");System.out.println(System.currentTimeMillis() - currentTimeMillis);//调用interrupt方法之后,中断标志位为trueSystem.out.println(Thread.currentThread().isInterrupted());}});thread.start();//开放或者注释该行代码,观察end park时间Thread.sleep(2000);//使用interrupt,也可以中断因为park造成的阻塞,但是该中断不会抛出异常thread.interrupt();}

3.3.4 park broker测试

/*** park broker测试*/public static void main(String[] args) {//分别尝试注释这两行代码,运行程序,运行cmd,使用jps 命令,找到该进程对应的pid,然后使用jstack pid 命令,就可以看到线程信息.//LockSupport.park();LockSupport.park(new LockSupportTest());}

分别注释其中一个方法,获得结果如下(找到main线程):

使用park,不能看到boroker信息:

使用park(broker),可以看到broker信息,因此推荐使用该方法阻塞线程:

4 LockSupport的底层实现原理

4.1 Unsafe

在LockSupport的原理部分,我们说道:“每个线程都与一个许可(permit)关联”。这句话,如果不深究,那么是没有问题的,底层的实现也确实和这个“permit”有关,但是不太准确。

如果你尝试在Thread实现类中去查找有没有这个permit属性或者与permit相关的属性,那么肯定让你大失所望,你会发现根本没有这个属性,那么,线程到底是在哪里与这个permit关联的呢?

上面我们“学习”了LockSupport的方法和源码,但是你会发现“异常的简单”,并且你会发现,所有类型的park和unpark方法啊最终都指向unsafe中的方法:

/*** 位于Unsafe中的方法* 释放被park阻塞的线程,也可以被使用来终止一个先前调用park导致的阻塞,即这两个方法的调用顺序可以是先unpark再park。** @param thread 线程*/public native void unpark(Object thread);/*** 位于Unsafe中的方法* 阻塞当前线程直到一个unpark方法出现(被调用)、一个用于unpark方法已经出现过(在此park方法调用之前已经调用过)、线程被中断或者time时间到期(也就是阻塞超时)、或者虚假唤醒。* 在time非零的情况下,如果isAbsolute为true,time是相对于新纪元(1970年)之后的毫秒,否则time表示当对当前的纳秒时间段。** @param isAbsolute 是否是绝对时间,true 是 false 否* @param time 如果是绝对时间,那么表示毫秒值,否则表示相对当前时间的纳秒时间段*/public native void park(boolean isAbsolute, long time);

可以看到这两个方法 都是native方法,即“本地方法”或者JNI,标识着通过这个方法可以使得Java 与 本地其他类型语言(如C、C++)直接交互。

Unsafe这个类中有许多的native方法,通过字段偏移量(类似于C的指针),提供了Java语言与底层系统进行交互的接口,通过Unsafe可以直接操作底层系统,它具有直接内存管理、线程阻塞&唤醒的支持、CAS操作的支持、直接操作类、对象、变量等强大的功能:JUC—Unsafe类的原理详解与使用案例

Unsafe的native方法的具体实现是交给Hotspot来实现的,因此我们必须去看看Hotspot的源码,而我们使用Oracle JDK并不提供Hotspot的源码,为此我们只有去Openjdk中查找,我们去Openjdk8中就能找到Unsafe的实现了。

现在,我们来到了C++的世界。下面的代码涉及到Hotspot的源码以及C++的语法,如果觉得确实看起来比较吃力那么请谨慎观看,对于普通人来说,了解LockSupport的原理到此也就足够了。

首先给出Openjdk8种Unsafe的Java实现:(https://hg./jdk8u/jdk8u/jdk/file/3ef3348195ff/src/share/classes/sun/misc/Unsafe.java)。虽然没有源码,但是有了注释,我们还是能看懂它的功能和作用。然后是C++的实现(http://hg./jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/prims/unsafe.cpp)。

//park方法UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time))//…………//调用的parker的park方法thread->parker()->park(isAbsolute != 0, time);//…………UNSAFE_ENDUNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread))//…………//调用的parker的unpark方法p->unpark();//…………UNSAFE_END

我们可以找到,最终会调用Parker的park和unpark方法。

4.2 Thread

我们首先应该明白,我们创建调用thread.start方法,底层系统做了什么,实际上start方法最终也会调用JNI方法,这将会创建一个C++实现的JavaThread实例,JavaThread在JVM中表示JVM的线程,JavaThread会通过POSIX接口create_thread创建一个OSThread实例,OSThread在OS中表示原生线程。Thread实例、JavaThread实例、OSThread实例是一对一的关系。start创建之后OSThread会执行JavaThread的run方法,这个方法又会执行Thread的run方法。

首先是Hotspot中各种Thread实现的通用Thread(http://hg./jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/thread.hpp)父类:

class Thread: public ThreadShadow {protected:// OS data associated with the threadOSThread* _osthread; // Platform-specific thread information//…………public: ParkEvent * _ParkEvent ; // for synchronized()ParkEvent * _SleepEvent ;// for Thread.sleepParkEvent * _MutexEvent ;// for native internal Mutex/MonitorParkEvent * _MuxEvent ; // for low-level muxAcquire-muxRelease//…………}

在里面我们能找到某些关键的字段信息,比如_osthread,这是对应着底层原生OSThread线程,然后还有一些ParkEvent类型的属性,这些属性在这篇文章中没啥用,但是作为扩展,ParkEvent实际上对应着Java的synchronized关键字在JVM层面的实现,同时也实现wait、notify、sleep功能,我们的synchronized的实现的文章中会深入分析这里的源码,简单的说就是实现多线程同步(锁),在ObjectWaiter的实现中,也有ParkEvent属性。

然后我们来看JavaThread的实现,同样在thread.hpp文件中:

class JavaThread: public Thread {private:JavaThread* _next;// The next thread in the Threads listoop _threadObj; // The Java level thread object// JSR166 per-thread parkerprivate:Parker* _parker;public:Parker*parker() {return _parker; }};

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层面的实现。

4.3 Parker

在Thread的源文件(http://hg./jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/thread.cpp)中,在创建JavaThread实例时会初始化Parker实例:

// ======= JavaThread ========// A JavaThread is a normal Java threadvoid JavaThread::initialize() {// Initialize fields// …………//调用Parker的Allocate方法,传递当前JavaThread线程_parker = Parker::Allocate(this) ;}

下面来看看Parker(http://hg./jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/park.hpp)的实现:

class Parker : public os::PlatformParker {private://计数,实际上这就是所谓的“permit许可”volatile int _counter ;//下一个ParkerParker * FreeNext ;//Parker关联的线程JavaThread * AssociatedWith ;public:Parker() : PlatformParker() {//初始化许可为0_counter = 0 ;FreeNext = NULL ;AssociatedWith = NULL ;}protected:~Parker() {ShouldNotReachHere(); }public:// For simplicity of interface with Java, all forms of park (indefinite,// relative, and absolute) are multiplexed into one call.//实际上park和unpark最终会调用Parker的同名方法void park(bool isAbsolute, jlong time);void unpark();// Lifecycle operators//接受一个线程,返回一个新的parker。这就是JavaThread的init时初始化Parker的方法static Parker * Allocate (JavaThread * t) ;static void Release (Parker * e) ;private:static Parker * volatile FreeList ;static volatile int ListLock ;};

Parker有一个_counter字段,这个字段实际上就是我们常说的“许可”,并且默认初始化为0。我们调用的park、unpark方法,实际上是调用的Parker的同名方法。

到此我们终于找到了常说的“许可”的真正实现!下面来看看park和unpark的底层原理!

4.4 PlatformParker

从Parker源码中还能看出Parker继承了PlatformParker,注意由于Hotspot虚拟机为跨平台,针对不同操作系统有不同的实现,我们最常见的就是linux系统,我们来看看linux下的PlatformParker(http://hg./jdk8/jdk8/hotspot/file/87ee5ee27509/src/os/linux/vm/os_linux.hpp)实现:

class PlatformParker : public CHeapObj<mtInternal> {protected:enum {REL_INDEX = 0,ABS_INDEX = 1};//条件变量数组的下标索引//-1表示初始化值,即当前没有使用条件变量//0表示数组第一个条件变量,用于park相对时间的线程挂起//1表示数组第二个条件变量,用于park绝对时间的线程挂起int _cur_index; // which cond is in use: -1, 0, 1//mutex 底层线程同步工具:互斥锁pthread_mutex_t _mutex [1] ;//condition 底层线程同步工具:条件变量。这里有两个,一个是相对时间,另一个是绝对时间pthread_cond_t _cond [2] ; // one for relative times and one for abs.public: // TODO-FIXME: make dtor private~PlatformParker() {guarantee (0, "invariant") ; }public:PlatformParker() {int status;//初始化_mutex和_condstatus = pthread_cond_init (&_cond[REL_INDEX], os::Linux::condAttr());assert_status(status == 0, status, "cond_init rel");status = pthread_cond_init (&_cond[ABS_INDEX], NULL);assert_status(status == 0, status, "cond_init abs");status = pthread_mutex_init (_mutex, NULL);assert_status(status == 0, status, "mutex_init");//这里_cur_index初始化为-1_cur_index = -1; // mark as unused}};

PlatformParker内部具有POSIX库标准的互斥量(锁)mutex和条件变量condition,那么实际上Parker的对于park和unpark的实现实际上就是用这两个工具实现的。

另外,PlatformParker还有一个_cur_index属性,它的值为-1、0或者1,-1时初始化的值,调用park并返回的线程也会设置值为-1。如果不是-1,那么表示对应的parker中的条件变量上有线程被挂起,_cur_index等于0表示调用park相对时间的线程在第一个条件变量上被挂起,等于1则表示调用park绝对时间的线程在第二个条件变量上被挂起。

4.5 mutex与condition概述

上面提到了mutex与condition,实际上mutex与condition都是posix标准的用于底层系统线程实现线程同步的工具。mutex被称为互斥量锁,类似于Java的锁,即用来保证线程安全,一次只有一个线程能够获取到互斥量mutex,获取不到的线程则可能会阻塞。而这个condition可以类比于java的Condition,被称为条件变量,用于将不满足条件的线程挂起在指定的条件变量上,而当条件满足的时候,再唤醒对应的线程让其执行。

Condition的操作本身不是线程安全的,没有锁的功能,只能让线程等待或者唤醒,因此mutex与Condition常常一起使用,这又可以类比Java中的Lock与Condition,或者synchronized与监视器对象。通常是线程获得mutex锁之后,判断如果线程不满足条件,则让线程在某个Condition上挂起并释放mutex锁,当另一个线程获取mutex锁并发现某个条件满足的时候,可以将调用Conditon的方法唤醒在指定Conditon上等待的线程并获取锁,然后被唤醒的线程由于条件满足以及获取了锁,则可以安全并且符合业务规则的执行下去。

mutex与condition的实现,实际他们内部都使用到了队列,可以类比Java中AQS的同步队列和条件队列。同样,在condition的条件队列中被唤醒的线程,将会被放入同步队列等待获取mutex锁,当获取到所之后,才会真正的返回,这同样类似于AQS的await和signal的实现逻辑。

可以看到,实际上JUC中的AQS框架的实现借鉴了底层系统的mutex和condition,如果我们理解了AQS的实现,那么理解mutex和condition的关系就很简单了。他们的区别就是AQS是采用Java语言实现的,而mutex和condition是系统工具,采用C++实现的。AQS中线程的阻塞park和唤醒unpark同样用到了mutex和condition的方法调用。AQS:JUC—AbstractQueuedSynchronizer(AQS)五万字源码深度解析与应用案例。

这里并没有讲mutex与condition的源码实现,在后面的文章中会讲到!

4.6 park方法

接下来我们就可以看park与unpark的实现了。在Hotspot虚拟机中,这两个方法并没有统一的实现,而是不同的操作系统具有自己的实现。一般我们使用的linux系统,因此这里我们来看看linux系统(http://hg./jdk8/jdk8/hotspot/file/87ee5ee27509/src/os/linux/vm/os_linux.cpp)的park与unpark实现。

我们首先看看linux系统下park的实现,大概步骤如下:

首先检查许可_counter是否大于0,如果是那么表示此前执行过unpark,那么将_counter重置为0,直接返回,此时没有并且也不需要获取mutex。如果当前线程被中断了,那么直接返回。如果time时间值小于0,或者是绝对时间并且time值等于0,那么也直接返回。如果当前线程被中断了,那么直接返回,否则非阻塞式的获取mutex锁,如果没有获取到,那么表示此时可能有其他线程已经在unpark该线程并获取了mutex锁,那么也直接返回。获取到了锁之后,再次判断_counter是否大于0,如果是,那么表示已经有了许可,那么将_counter置为0,释放mutex锁,然后返回。根据参数设置_cur_index的值(0或1)并调用pthread_cond_wait 或者safe_cond_timedwait进入对应的条件变量等待,并自动释放 mutex 锁。此时后续代码不会执行。被唤醒后,并没有主动获取mutex 锁,因为内核会自动帮我们重新获取 mutex 锁,将 _counter重置为 0,表示消耗了许可;将_cur_index 重置为-1,表示没有线程在等待。park方法结束。

/*isAbsolute 是否是绝对时间time 如果是绝对时间,那么表示自格林尼治标准时间以来的毫秒值,否则表示相对当前时间的纳秒时间段*/void Parker::park(bool isAbsolute, jlong time) {// Ideally we'd do something useful while spinning, such// as calling unpackTime().// Optional fast-path check:// Return immediately if a permit is available.// We depend on Atomic::xchg() having full barrier semantics// since we are doing a lock-free update to _counter.//CAS操作,如果_counter大于0,则将_counter置为0,直接返回,否则表示_counter为0if (Atomic::xchg(0, &_counter) > 0) return;//获取当前线程ThreadThread* thread = Thread::current();assert(thread->is_Java_thread(), "Must be JavaThread");//将线程强转为JavaThreadJavaThread *jt = (JavaThread *)thread;// Optional optimization -- avoid state transitions if there's an interrupt pending.// Check interrupt before trying to wait//如果当前线程已经设置了中断标志,则park方法直接返回if (Thread::is_interrupted(thread, false)) {return;}// Next, demultiplex/decode time argumentstimespec absTime;//如果time时间值小于0,或者是绝对时间并且time值等于0,那么也直接返回if (time < 0 || (isAbsolute && time == 0) ) {// don't wait at allreturn;}//如果如果time时间值大于0,那么计算定时时间(根据isAbsolute设置时间精度的)if (time > 0) {unpackTime(&absTime, isAbsolute, time);}// Enter safepoint region// Beware of deadlocks such as 6317397.// The per-thread Parker:: mutex is a classic leaf-lock.// In particular a thread must never block on the Threads_lock while// holding the Parker:: mutex. If safepoints are pending both the// the ThreadBlockInVM() CTOR and DTOR may grab Threads_lock.//构造一个ThreadBlockInVM对象,进入安全点,线程阻塞ThreadBlockInVM tbivm(jt);// Don't wait if cannot get lock since interference arises from// unblocking. Also. check interrupt before trying wait//如果当前线程被中断,那么直接返回//或者调用pthread_mutex_trylock尝试获取mutex互斥锁失败(返回0,任何其他返回值都表示错误),比如此时有线程已经先调用了unpark该线程并获取了mutex,那么直接返回//注意这里的pthread_mutex_trylock如果获取失败,也并不会阻塞,而是会马上返回一个非0的值if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {return;}//到这里表示获取互斥量mutex(加锁)成功,此时后续才能解锁int status ;//如果_counter大于0,说明存在“许可”,那么不必要再等待了if (_counter > 0) {// no wait needed//_counter置为0_counter = 0;//这一步释放互斥量(解锁),然后返回status = pthread_mutex_unlock(_mutex);assert (status == 0, "invariant") ;// Paranoia to ensure our locked and lock-free paths interact// correctly with each other and Java-level accesses.//这是实际上是一个storeload内存屏障指令,可以保证可见性,另外volatile写也是使用的这个屏障OrderAccess::fence();return;}#ifdef ASSERT// Don't catch signals while blocked; let the running threads have the signals.// (This allows a debugger to break into the running thread.)sigset_t oldsigs;sigset_t* allowdebug_blocked = os::Linux::allowdebug_blocked_signals();pthread_sigmask(SIG_BLOCK, allowdebug_blocked, &oldsigs);#endif//将操作系统线程设置为CONDVAR_WAIT状态,注意不是Object.wait()的状态,这是操作系统线程的状态OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);jt->set_suspend_equivalent();// cleared by handle_special_suspend_equivalent_condition() or java_suspend_self()assert(_cur_index == -1, "invariant");//如果时间为0,那么表示是相对时间,那么挂起线程if (time == 0) {_cur_index = REL_INDEX; // arbitrary choice when not timed//这里是使用的条件变量挂起线程,等待条件满则,需要互斥锁配合以防止多个线程同时请求pthread_cond_wait//同时释放_mutex锁//这里没有在while循环中调用pthread_cond_wait,可能会造成虚假唤醒status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;}/*否则,时间不为0*/else {//判断是相对时间还是绝对时间使用不同的参数_cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;//调用safe_cond_timedwait,表示计时等待,内部实际上调用了pthread_cond_timedwait方法;如果在给定时刻前条件没有满足,则返回ETIMEDOUT,结束等待//同时释放_mutex锁//这里没有在while循环中调用safe_cond_timedwait,可能会造成虚假唤醒status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ;//如果挂起失败if (status != 0 && WorkAroundNPTLTimedWaitHang) {//清除条件变量pthread_cond_destroy (&_cond[_cur_index]) ;//重新初始化条件变量pthread_cond_init (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr());}}/*下面是被唤醒之后的逻辑*/_cur_index = -1;assert_status(status == 0 || status == EINTR ||status == ETIME || status == ETIMEDOUT,status, "cond_timedwait");#ifdef ASSERTpthread_sigmask(SIG_SETMASK, &oldsigs, NULL);#endif//_counter许可重置为0_counter = 0 ;//释放互斥量(锁)status = pthread_mutex_unlock(_mutex) ;assert_status(status == 0, status, "invariant") ;// Paranoia to ensure our locked and lock-free paths interact// correctly with each other and Java-level accesses.//这是实际上是一个storeload内存屏障指令,可以保证可见性,另外volatile写也是使用的这个屏障OrderAccess::fence();// If externally suspended while waiting, re-suspend// 如果在线程被park挂起期间调用了stop或者suspend,那么调用java_suspend_self将继续线程挂起不if (jt->handle_special_suspend_equivalent_condition()) {jt->java_suspend_self();}}

Hotspot源码对于park方法的实现中,对于线程的挂起和唤醒都是利用了POSIX标准的mutex和condition工具,首先需要获取mutex互斥量锁,之后在进行条件变量的挂起操作,最后释放mutex互斥量锁。

我们还能明白,常说的“许可”实际上就是Parker类中的_counter属性,当存在许可:_counter>0,则park可以返回,并且在方法的最后必定消耗许可:将_counter置为0。

另外,调用park的线程如果没有返回,即被阻塞在某个条件变量上了,那么_cur_index(这个属性在PlatformParker中等一)将不等于-1;在线程返回之后,在park方法的最后又会将_cur_index置为-1。

4.6.1 虚假唤醒(spurious wakeup)

如果存在多条线程使用同一个_counter,那么进行挂起的方法pthread_cond_wait和safe_cond_timedwait的调用必须使用while循环包裹,在被唤醒之后,判断条件是否真的满足,否则可能被唤醒的同时其他线程消耗了条件导致不满足,这时就发生了“虚假唤醒”,即虽然阻塞的线程被唤醒了,但是实际上条件并不满足,那么此时需要继续等待。比如这样的写法就是正确的:

while(_counter==0){status = pthread_cond_wait();}

但是在park方法中,pthread_cond_wait和safe_cond_timedwait方法仅会被调用一次,并没有死循环包裹,这是因为一条线程对应一个Parker实例,不同的线程具有不同的Parker,每个Parker中的_counter仅仅记录当前绑定的线程的许可计数,虽然Parker仍然可能会由多个线程竞争(因为需要由其他线程通过unpark方法控制Parker绑定的线程的唤醒),但某个线程的pthread_cond_wait和safe_cond_timedwait方法(也就是park方法)不存在多线程竞争调用的可能,因为调用park方法的线程都是把自己进行wait,所以也没必要使用while循环,如果某线程被唤醒一般就是其他线程调用了针对此线程的unpark方法,此时许可一般都是充足的,这样看来不使用while循环确实没什么问题。但是,某些极端情况下仍然会造成“虚假唤醒(spurious wakeup)”,这时即使许可不足,那么仍然可以从park方法返回。

在park方法只是调用线程进行wait的情况下仍然可能“虚假唤醒”的原因主要是在linux环境下,在Condition的条件队列中wait的线程,即使没有signal或者signalAll的调用,wait也可能返回。因为这里线程的阻塞通常是使用一些底层工具实现的,比如Futex组件,如果这是底层组件进程被中断,那么会终止线程的阻塞,然后直接返回EINTR错误状态。这也是在park方法中写到的返回的第三个原因:

但是这情况几乎见不到,这里写出来仅仅是声明有这种可能而已。

4.7 unpark方法

unpark相对park方法来说简单了不少,它的实现同样在os_linux.cpp文件中,大概步骤为:

首先阻塞式的获取mutex锁,获取不到则一直阻塞在此,直到获取成功。获取到mutex锁之后,获取当前的许可_counter的值保存在变量s中,然后将_counter的值置为1。如果s小于1,表示没有了许可,此时可能存在线程被挂起,也可能不存在,继续向下判断: 如果_cur_index不为-1,那么肯定有在_cur_index对应索引的条件变量上挂起,那么需要唤醒:如果设置了WorkAroundNPTLTimedWaitHang(linux默认设置),那么先signal唤醒在条件变量上等待的线程然后释放mutex锁,方法结束;否则先释放mutex锁然后signal唤醒在条件变量上等待的线程,方法结束。否则_cur_index等于-1,表示没有线程在条件变量上等待,直接释放mutex锁,方法结束。 否则,s等于1,表示一直存在许可,那么就什么都不做,仅仅是unlock释放mutex锁就行了,方法结束。

/*提供一个许可*/void Parker::unpark() {int s, status ;//类似于park,阻塞式的获取互斥量(锁),表示已上锁,如果互斥量已被获取,该线程将在该方法处阻塞,直到获取成功status = pthread_mutex_lock(_mutex);assert (status == 0, "invariant") ;//保存旧的_counters = _counter;//将_counter置为1,这里也能看出来无论调用多少次unpark,“许可”都不会变得更多_counter = 1;//如果原来的_counter为0,表示没有了许可,此时可能存在线程被挂起,也可能不存在if (s < 1) {// 如果_cur_index不等于初始值-1,那么表示有线程在当前parker的对应的条件变量上挂起了//_cur_index为0,则是因为调用相对时间的park方法,在第一个条件变量上挂起,//_cur_index为1,则是因为调用绝对时间的park方法,在第二个条件变量上挂起,if (_cur_index != -1) {// thread is definitely parked/*如果设置了WorkAroundNPTLTimedWaitHang,那么先调用signal再调用unlock,否则相反*///WorkAroundNPTLTimedWaitHang是一个JVM参数,默认为1if (WorkAroundNPTLTimedWaitHang) {//先signal唤醒一条在指定条件变量上等待的线程status = pthread_cond_signal (&_cond[_cur_index]);assert (status == 0, "invariant");//再unlock释放互斥量(锁)status = pthread_mutex_unlock(_mutex);assert (status == 0, "invariant");}/*否则就是先unlock 再signal*/else {//先unlock释放互斥量(锁)status = pthread_mutex_unlock(_mutex);assert (status == 0, "invariant");//再signal唤醒一条在指定条件变量上等待的线程status = pthread_cond_signal (&_cond[_cur_index]);assert (status == 0, "invariant");}}/*否则,表示没有线程在条件变量上等待,仅仅是unlock释放互斥量(锁)就行了,因为park方法返回的时候会设置_cur_index为-1*/else {pthread_mutex_unlock(_mutex);assert (status == 0, "invariant") ;}}/*否则,表示原来的_counter为1,表示一直存在许可,那么仅仅unlock释放互斥量(锁)就行了*/else {pthread_mutex_unlock(_mutex);assert (status == 0, "invariant") ;}}

5 LockSupport的总结

LockSupport是JDK1.5时提供的用于实现单个线程等待、唤醒机制的阻塞工具,也是AQS框架的基石,另两个则是CAS操作、volatile关键字。

关于Java中CAS和volatile的底层原理,在前面的章节已经解析过了,本文是LockSupport的原理,也就是JUC中线程park阻塞、unpark唤醒的机制的底层实现原理(注意这和synchronized的wait()阻塞、notify()唤醒的原理是有区别的)。通过CAS、LockSupport以及volatile,我们就可以使用Java语言实现锁的功能,也就是JUC中的AQS。

LockSupport和CAS方法则是调用了Unsafe类的JNI方法,最终Unsafe的方法由Hotspot等虚拟机实现,另外volatile关键字则是在编译的时候会加上特殊访问标记,JVM在执行字节码的时候,也会做出相应的处理。实际上Java中线程的各种阻塞、唤醒、同步、睡眠等底层机制都是JVM层面实现的,但是这还没完,在JVM中通常会再深入调用一些POSIX的系统函数(比如mutex、Condition等工具和方法,这些都是操作系统提供的),最终会执行到操作系统级别,Java层面大多数都是提供了可调用的接口和一些简单的逻辑。

执行LockSupport.park方法不会释放此前获取到的synchronized锁或者lock锁,因为LockSupport的方法根本就与我们常说的“锁”无关,无论有没有锁,你都可以在任何地方调用LockSupport的方法阻塞线程,它只与单个线程关联,因此仅仅依靠LockSupport也而不能实现“锁”的功能。

LockSupport的park和unpark方法在系统底层的实现都是依赖了mutex和Condition工具。

相关文章:

AQS:JUC—AbstractQueuedSynchronizer(AQS)五万字源码深度解析与应用案例。volatile:Java中的volatile实现原理深度解析以及应用。CAS:Java中的CAS实现原理解析与应用。UNSAFE:JUC—Unsafe类的原理详解与使用案例。

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

如果觉得《Java LockSupport以及park unpark方法源码深度解析》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。