JUC—LockSupport以及park、unpark方法底层源码深度解析

  详细介绍了JUC中的LockSupport阻塞工具以及park、unpark方法的底层原理,从Java层面深入至JVM层面。AQS框架是JUC中实现同步组件的基石,而LockSupport可以说是AQS框架的基石。

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);
    //执行到这一步,说明线程被唤醒了,此时清除broker
    setBlocker(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() {
        @Override
        public 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() {
        @Override
        public void run() {
            //最开始中断标志位位false
            System.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方法之后,中断标志位为true
            System.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_END


UNSAFE_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 thread
  OSThread* _osthread;  // Platform-specific thread information
 //…………
 public:  
  ParkEvent * _ParkEvent ;                     // for synchronized()
  ParkEvent * _SleepEvent ;                    // for Thread.sleep
  ParkEvent * _MutexEvent ;                    // for native internal Mutex/Monitor
  ParkEvent * _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 list
  oop            _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 Paeker

  在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 thread

void 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 ;
  //下一个Parker
  Parker * 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相对时间的线程挂起
    //0表示数组第二个条件变量,用于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和_cond
      status = 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为0
  if (Atomic::xchg(0, &_counter) > 0) return;
  //获取当前线程Thread
  Thread* thread = Thread::current();
  assert(thread->is_Java_thread(), "Must be JavaThread");
  //将线程强转为JavaThread
  JavaThread *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 arguments
  timespec absTime;
  //如果time时间值小于0,或者是绝对时间并且time值等于0,那么也直接返回
  if (time < 0 || (isAbsolute && time == 0) ) { // don't wait at all
    return;
  }
  //如果如果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 ASSERT
  pthread_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,对于_counter的使用,不存在多线程竞争,这样看来确实没什么问题。但是,仍然会造成“虚假唤醒(spurious wakeup)”,这时即使许可不足,那么仍然可以从park方法返回。
  在只有单个线程的情况下仍然可能“虚假唤醒”的原因主要是在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,表示没有了许可,此时可能存在线程被挂起,也可能不存在,继续向下判断:
    a) 如果_cur_index不为-1,那么肯定有在_cur_index对应索引的条件变量上挂起,那么需要唤醒:如果设置了WorkAroundNPTLTimedWaitHang(linux默认设置),那么先signal唤醒在条件变量上等待的线程然后释放mutex锁,方法结束;否则先释放mutex锁然后signal唤醒在条件变量上等待的线程,方法结束。
    b) 否则_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") ;
  //保存旧的_counter
  s = _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参数,默认为1
      if (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学习博客!

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/107207643