Java多线程【6】LockSupport park/unpark原理和使用以及于wait/notify的区别

系列文章目录

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方法的底层执行过程

  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方法结束。
    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底层执行

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王乐乐君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值