LockSupport分析

简介

LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有俩个函数

public native void unpark(Thread jthread);
 public native void park(boolean isAbsolute, long time);

isAbsolute参数是指明时间是绝对的,还是相对的。

仅仅两个简单的接口,就为上层提供了强大的同步原语。

先来解析下两个函数是做什么的。

unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。

比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。

注意,unpark函数可以先于park调用。比如线程B调用unpark函数,给线程A发了一个“许可”,那么当线程A调用park时,它发现已经有“许可”了,那么它会马上再继续运行。

实际上,park函数即使没有“许可”,有时也会无理由地返回,这点等下再解析。

park和unpark的灵活之处
上面已经提到,unpark函数可以先于park调用,这个正是它们的灵活之处。

一个线程它有可能在别的线程unPark之前,或者之后,或者同时调用了park,那么因为park的特性,它可以不用担心自己的park的时序问题,否则,如果park必须要在unpark之前,那么给编程带来很大的麻烦!!

考虑一下,两个线程同步,要如何处理?

在Java5里是用wait/notify/notifyAll来同步的。wait/notify机制有个很蛋疼的地方是,比如线程B要用notify通知线程A,那么线程B要确保线程A已经在wait调用上等待了,否则线程A可能永远都在等待。编程的时候就会很蛋疼。
另外,是调用notify,还是notifyAll?

notify只会唤醒一个线程,如果错误地有两个线程在同一个对象上wait等待,那么又悲剧了。为了安全起见,貌似只能调用notifyAll了。

park/unpark模型真正解耦了线程之间的同步,线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态。

Hotspot实现

每个java线程都有一个parker实例,parker类的定义是这样的

class Parker : public os::PlatformParker {
private:
  volatile int _counter ;
  ...
public:
  void park(bool isAbsolute, jlong time);
  void unpark();
  ...
}
class PlatformParker : public CHeapObj<mtInternal> {
  protected:
    pthread_mutex_t _mutex [1] ;
    pthread_cond_t  _cond  [1] ;
    ...
}

可以看到parker类实际上用的Posix的mutex,cond来实现的

  • _counter就是是否阻塞的标记(许可)
  • _cond就好比阻塞后线程的容器
  • _mutex是互斥锁,线程虚持有后进入_cond

在Parker类里的_counter字段,就是用来记录所谓的许可的

当调用park时,先尝试直接能否直接拿到“许可”,即_counter>0时,如果成功,则把_counter设置为0,并返回:

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.
  if (Atomic::xchg(0, &_counter) > 0) return;

如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,unlock mutex并返回:

ThreadBlockInVM tbivm(jt);
  if (_counter > 0)  { // no wait needed
    _counter = 0;
    status = pthread_mutex_unlock(_mutex);

否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0,unlock mutex并返回:

  if (time == 0) {
    status = pthread_cond_wait (_cond, _mutex) ;
  }
  _counter = 0 ;
  status = pthread_mutex_unlock(_mutex) ;
  assert_status(status == 0, status, "invariant") ;
  OrderAccess::fence();

当unpark时,则简单多了,直接设置_counter为1,再`unlock mutet返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程

void Parker::unpark() {
  int s, status ;
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant") ;
  s = _counter;
  _counter = 1;
  if (s < 1) {
     if (WorkAroundNPTLTimedWaitHang) {
        status = pthread_cond_signal (_cond) ;
        assert (status == 0, "invariant") ;
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant") ;
     } else {
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant") ;
        status = pthread_cond_signal (_cond) ;
        assert (status == 0, "invariant") ;
     }
  } else {
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
  }
}

换而言之,是用mutex和cond保护了一个_counter的变量,当park时,这个变量设置为了0,当unpark时,这个变量设置为1

流程

park方法

1、执行park()方法
2、检查_counter是否是0
3、如果_counter是0,则获取互斥锁_mutex
4、获取到互斥锁,进入_cond中进行阻塞
5、再次设置_counter为0

unpark()

1、执行unpark(thread-1),设置_counter=1
2、唤醒_cond中阻塞的线程thread-1
3、线程thread-1恢复运行
4、再次设置_counter = 0

先调用unpark,再调用park

1.调用Unsafe.unpark(Thread_0)方法,设置_counter为1
2.当前线程调用Unsafe.park()方法
3.检查_counter,本情况为1,这时线程无需阻塞,继续运行
4.设置_counter为0

优势

与 Object 的 wait/notify 相比:

wait,notify 和 notifyAll 必须配合 Monitor(重量级锁) 一起使用,而 park,unpark 不必。

park/unpark 是以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,无法做到精确唤醒。

park/unpark 可以先 unpark,而 wait/notify 不能先 notify。

Last modification:January 18, 2024
如果觉得我的文章对你有用,请随意赞赏