详细介绍了JUC中的LockSupport阻塞工具以及park、unpark方法的底层原理,从Java层面深入至JVM层面。AQS框架是JUC中实现同步组件的基石,而LockSupport可以说是AQS框架的基石。
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框架的基石之一。
每个线程都与一个许可(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状态。
以上只是非常简单易懂的原理,后面会有详细的解释!
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);
}
在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;
//……
}
/**
* 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);
}
/**
* 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
/**
* 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();
}
/**
* 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信息,因此推荐使用该方法阻塞线程:
在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方法。
我们首先应该明白,我们创建调用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层面的实现。
在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的底层原理!
从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绝对时间的线程在第二个条件变量上被挂起。
上面提到了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的源码实现,在后面的文章中会讲到!
接下来我们就可以看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的实现,大概步骤如下:
/*
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。
如果存在多条线程使用同一个_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方法中写到的返回的第三个原因:
但是这情况几乎见不到,这里写出来仅仅是声明有这种可能而已。
unpark相对park方法来说简单了不少,它的实现同样在os_linux.cpp文件中,大概步骤为:
/*
提供一个许可
*/
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") ;
}
}
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学习博客!