介绍了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 Unsafe
    • 4.2 Thread
    • 4.3 Parker
    • 4.4 PlatformParker
    • 4.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 特征

  1. LockSupport是非重入的,这个很简单,因为park的意思仅仅是阻塞某个线程而已,并不是“锁”,调用一次park方法,线程就被阻塞了。
  2. LockSupport的park阻塞、unpark唤醒的调用不需要任何条件对象,也而不需要先获取什么锁。在一定程度上降低代码的耦合度,即LockSupport只与线程绑定,并且被park的线程并不会释放之前获取到的锁。
  3. park阻塞与unpark唤醒的调用顺序可以颠倒,不会出现死锁,并且可以重复多次调用unpark;而stop和resume方法如果顺序反了,就会出现死锁现象。
  4. 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测试*/
@Test
public void test2() {System.out.println("begin park");//调用park方法LockSupport.park();//使当前线程获取到许可证,明显执行不到这一步来,因为在上一步就已经阻塞了LockSupport.unpark(Thread.currentThread());System.out.println("end park");
}/*** park/unpark测试*/
@Test
public void test3() {System.out.println("begin park");//使当前线程先获取到许可证LockSupport.unpark(Thread.currentThread());//再次调用park方法,先获得了许可,因此该方法不会阻塞LockSupport.park();System.out.println("end park");
}/*** park/unpark测试*/
@Test
public 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*/
@Test
public 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中断测试*/
@Test
public 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.openjdk.java.net/jdk8u/jdk8u/jdk/file/3ef3348195ff/src/share/classes/sun/misc/Unsafe.java)。虽然没有源码,但是有了注释,我们还是能看懂它的功能和作用。然后是C++的实现(http://hg.openjdk.java.net/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.openjdk.java.net/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 parker
private: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.openjdk.java.net/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.openjdk.java.net/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.openjdk.java.net/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.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/os/linux/vm/os_linux.cpp)的park与unpark实现。

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

  1. 首先检查许可_counter是否大于0,如果是那么表示此前执行过unpark,那么将_counter重置为0,直接返回,此时没有并且也不需要获取mutex。
  2. 如果当前线程被中断了,那么直接返回。
  3. 如果time时间值小于0,或者是绝对时间并且time值等于0,那么也直接返回。
  4. 如果当前线程被中断了,那么直接返回,否则非阻塞式的获取mutex锁,如果没有获取到,那么表示此时可能有其他线程已经在unpark该线程并获取了mutex锁,那么也直接返回。
  5. 获取到了锁之后,再次判断_counter是否大于0,如果是,那么表示已经有了许可,那么将_counter置为0,释放mutex锁,然后返回。
  6. 根据参数设置_cur_index的值(0或1)并调用pthread_cond_wait 或者safe_cond_timedwait进入对应的条件变量等待,并自动释放 mutex 锁。此时后续代码不会执行。
  7. 被唤醒后,并没有主动获取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文件中,大概步骤为:

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

相关文章:

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

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

Java LockSupport以及park、unpark方法源码深度解析相关推荐

  1. php的setinc方法,thinkphp3.2.0 setInc方法 源码全面解析

    搜索热词 我们先来看一下setInc的官方示例: 需要一个字段和一个自增的值(默认为1) 我们通过下面这个例子来一步步分析他的底层是怎么实现的: class TestController extend ...

  2. Go netpoll I/O 多路复用构建原生网络模型之源码深度解析

    原文 Go netpoll I/O 多路复用构建原生网络模型之源码深度解析 导言 Go 基于 I/O multiplexing 和 goroutine 构建了一个简洁而高性能的原生网络模型(基于 Go ...

  3. Spring源码深度解析(郝佳)-学习-源码解析-基于注解bean定义(一)

    我们在之前的博客 Spring源码深度解析(郝佳)-学习-ASM 类字节码解析 简单的对字节码结构进行了分析,今天我们站在前面的基础上对Spring中类注解的读取,并创建BeanDefinition做 ...

  4. 《Spring源码深度解析 郝佳 第2版》AOP

    往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...

  5. 《Spring源码深度解析 郝佳 第2版》SpringBoot体系分析、Starter的原理

    往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...

  6. Spring源码深度解析(郝佳)-学习-源码解析-创建AOP静态代理实现(八)

    继上一篇博客,我们继续来分析下面示例的 Spring 静态代理源码实现. 静态 AOP使用示例 加载时织入(Load -Time WEaving,LTW) 指的是在虚拟机载入字节码时动态织入 Aspe ...

  7. Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析

      我们在看Spring Boot源码时,经常会看到一些配置类中使用了注解,本身配置类的逻辑就比较复杂了,再加上一些注解在里面,让我们阅读源码更加难解释了,因此,这篇博客主要对配置类上的一些注解的使用 ...

  8. Spring源码深度解析(郝佳)-学习-源码解析-基于注解切面解析(一)

    我们知道,使用面积对象编程(OOP) 有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共的行为时,例如日志,安全检测等,我们只有在每个对象引用公共的行为,这样程序中能产生大量的重复代码,程序就 ...

  9. 《Spring源码深度解析 郝佳 第2版》JDBC、MyBatis原理

    往期博客 <Spring源码深度解析 郝佳 第2版>容器的基本实现与XML文件的加载 <Spring源码深度解析 郝佳 第2版>XML标签的解析 <Spring源码深度解 ...

最新文章

  1. vivo手机解锁_关于手机“解锁”的话题,vivo屏幕指纹技术很有话语权
  2. vpython 贞测碰撞_7、Pygame碰撞检测
  3. 手游运营重度化,抓好论坛专区“预热战场”
  4. BZOJ1856:[SCOI2010]字符串
  5. 低代码,填补业务技术鸿沟 or 紧贴业务的开发时代?
  6. java浮点型需知_java使用数字类型注意事项
  7. 分布式SOA基础架构崭露头角
  8. libcurl 多线程使用注意事项 - Balder~专栏 - 博客频道 - CSDN.NET
  9. java文件下载代码_Java代码实现文件下载
  10. 历时3个月终克2.1大礼包
  11. Raki的读paper小记:Rational LAMOL: A Rationale-Based Lifelong Learning Framework
  12. #USB加密狗信息安全与USB_Host 硬件读写加密狗
  13. mfc动态改变clip风格_欧式古典家具风格的演变历程
  14. 自己打羽毛球的若干问题
  15. 水星路器服务器无响应,水星路由器无法上网怎么办?
  16. 图像处理——图像的增强技术
  17. 【慕伏白教程】Git的安装与基本使用
  18. matlab怎么画两个自变量的图_眼线液的画步骤图 眼线怎么画好看图解
  19. nexus5 博通芯片WIFI详解 (2)
  20. 【精品】机器学习模型可解释的重要及必要性

热门文章

  1. 基于74LS148用Multisim仿真做8路抢答器
  2. stateflow基本操作
  3. uniapp实现canvas制作微信小程序海报,线上图片
  4. CF进制转换专题进阶
  5. Arduino智能物流小车各部分功能代码详解(省工程训练能力综合竞赛)
  6. 机器学习算法之集成方法
  7. 如何在电脑使用计算机,Windows To Go 如何在电脑上使用?
  8. 区块链投资需要多少钱
  9. JAVA打印空三角形
  10. 百度智能云数字人凭什么领跑中国AI数字人?