系列文章目录
Java多线程【1】synchronized对象锁、内置锁使用
Java多线程【2】Java wait/notify的使用于同步模式保护性暂停
Java多线程【3】同步模式之保护性暂停案例 相亲问题
Java多线程【4】interrupt线程的打断机制、两阶段终止模式
Java多线程【5】异步模式之生产者消费者
Java多线程【6】LockSupport park/unpark原理和使用以及于wait/notify的区别
文章目录
前言
LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了主要的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里。和wait/notify不同,LockSupport对于java的monitor管程底层使用的是不同的方法,后文我会详细解释。
一、paker
在每一个java线程被创建的时候都会创建一个Parker实例。当然这个实例可以用过park(object)的方式替换,但一个线程有且只会有一个Parker实例,但一个Parker实例可以被多个线程使用(线程售使用unpark方法)
1.Parker源码
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 ;
};
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和_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
}
};
2.Parker组成
1.1 _counter
也被称为许可,默认初始化为0。
1.2 _mutex
底层线程同步工具:互斥锁。
1.3 _cond
底层线程同步工具:条件变量。
3.Parker和线程的关系
线程实例化的时候会初始化一个Parker实例。这个Parker实例会传入当前线程,调用Parker的Allocate方法,传递当前JavaThread线程_parker = Parker::Allocate(this)
void JavaThread::initialize() {
// Initialize fields
// …………
_parker = Parker::Allocate(this) ;
}
二、park
1、系统底层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的实现逻辑。
2、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();
}
}
3、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方法结束。
Hotspot源码中,调用了c++程序的pthread_cond_wait
方法,这个方法会将当前线程加入操作系统的阻塞队列中使线程等待,并释放mutex锁,等待其他线程获取mutex锁幻想阻塞队列中的线程。
4、Java代码中park使用
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Object parker = new Object();
//不指定parker对象
LockSupport.park();
//制定parker对象
LockSupport.park(parker);
}
});
三、unpark
1、unpark源码
/*
提供一个许可
*/
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") ;
}
}
2、unpark底层执行
- 首先阻塞式的获取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锁就行了,方法结束。
3、Java代码中unpark使用
Thread thread = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
TimeUnit.SECONDS.sleep(2);
LockSupport.park();
}
});
thread.start();
LockSupport.unpark(thread);
总结
1.park/unpark和wait/notify的区别
这篇文文章详细的介绍了synchronized的原理和重量级锁,可以看一下synchronized原理之轻量级锁、重量级锁
1.1 是否需要获取锁(Monitor)
park/unpark
不需要获取锁,因为他们只是简单是单个线程间的的线程挂起和唤醒,不存在上下文切换,所以也不会到Monitor的重量级锁。wait/notify
需要获取锁,被wait的线程需要在Monitor对象的waitSet等待被notify唤醒。
1.2 park后是否会释放锁
我们知道,在调用wait方法后线程会释放锁,但是上面讲到park不需要锁,而且只是单线程之间的唤醒和挂起,是不会涉及到锁的概念,自然不会释放锁。
1.3 作用对象
park
作用于调用LockSupport.park方法的当前线程,unpark
作用于调用LockSupport.park(thread)方法传入的线程,是属于单线程间的。
wait/notify
作用于锁对象(Monitor),针对的是所有需要获取该锁的线程,属于是多个线程间的,有上下文切换
1.4 调用顺序
park/unpark
不需要注意先后顺序,哪怕先unpark然后执行park线程也会被唤醒。wait/notify则不行,暂停和唤醒必须按照严格的先后顺序
1.5 效率
park/unpark
效率一定程度上高于wait/notify