park/unpark 拾遗


背景

看condition源码的时候,park这里没有明白,到底是怎么唤醒的,是立即唤醒,还是park自旋自动唤醒?
这个还没查清楚,结果看有的博文,把我彻底搞晕了(就是我下面的),所以想弄明白到底怎么回事

概览

本质是0/1之间的切换,这里的0/1就是permit,理解为许可,一个线程最多只有一个permit,park消耗1->0,unpark增加0->1

不会死锁

permit不会叠加,不要连续调用两次unpark,相当于调用了一次unpark,除非park之后permit被消耗为0,再去调用一次unpark才可以继续变为1才可以。

一般配置自旋使用,当然没有自旋也可以,因为在park哪里已经阻塞住了,通过unpark唤醒是也可以的。
while (!canProceed()) { … LockSupport.park(this); }}

看下踩坑的总结

之前读过一篇博文其中有一句这样的话,搞了我一天,最后不得不弄懂park的原理,强迫症啊

而如果线程A处于等待许可状态,再次调用park,则会永远等待下去,调用unpark也无法唤醒。https://blog.csdn.net/anLA_/article/details/78635300

包括这篇文章

如果一个线程连续2次调用LockSupport.park(),那么该线程一定会一直阻塞下去。https://blog.csdn.net/aitangyong/article/details/38373137

https://www.cnblogs.com/set-cookie/p/9582547.html 在这个文章中有一个Demo,和这里说的意思大概差不多,我进行了稍微的改造:

package com.leesin.heightConcurrent.park;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * @description:
 * @author: Leesin.Dong
 * @date: Created in 2020/3/17 19:08
 * @version:
 * @modified By:
 */
public class ParkDemo {
    public static void main(String[] args) {

        Thread thread1 = new Thread(() -> {
            System.out.println("开始");
            LockSupport.park();
            System.out.println("第一次park");
            // LockSupport.unpark(Thread.currentThread());
            LockSupport.park();
            System.out.println("第二次park");
        }, "Thread1");

        Thread thread2 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(thread1);
            LockSupport.unpark(thread1);
        }, "Thread2");

        Thread thread3 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(thread1);
        }, "Thread3");

        thread1.start();
        thread2.start();
        // thread3.start();
    }
}

如果关闭// thread3.start();,确实会出现
image.png
但是我打开// thread3.start();,就能解决问题了
image.png
并不是像上面说的那样

则会永远等待下去,调用unpark也无法唤醒。

总结

连续两次调用unpark,还是1,这个时候被第一个park消耗掉,第二个park处于阻塞状态,这样确实没有办法了。
但是可以再通过另一个线程再次unpark变成1,甚至在两次unpark之间让他sleep1s,也可以达到效果。

TimeUnit.SECONDS.sleep(1);
LockSupport.unpark(thread1);
TimeUnit.SECONDS.sleep(1);
LockSupport.unpark(thread1);

在第一个park和第二个park之间unpark一次即可

如果park() unpark(){thread2} unpark(){thread2} park() ,这样会在第二个park()处阻塞
park() unpark(){thread2} unpark(){thread3} park(),或者park() unpark(){thread2} unpark(){thread2 sleep n s} park()这样不会再第二个park处阻塞

再想想:
之前连续两次是因为中间有一个时间阈值,第一次unpark变为1之后还没来得及唤醒第一次park,即1还没来得及被消耗,又一次unpark,这次还是1,就相当于 unpark->unpark->park消耗掉。
如果第一次unpark之后,唤醒了park,permit被第一次park消耗掉变成0,这个时候,再次通过unpark就可以成为1,然后继续被第二次park消耗掉了,也就不会被阻塞住了。
所以关键是要找到这个时间阈值,所以这里的关键点不是thread3,而是sleep n s,这个n跟操作系统的调度时间有关。

关键点:只要unpark被消耗掉,就可以再次park了。

源码

park

void Parker::park(bool isAbsolute, jlong time) {
  // 先原子的将_counter的值设为0,并返回_counter的原值,如果原值>0说明有通行证,直接返回
  if (Atomic::xchg(0, &_counter) > 0) return;

  Thread* thread = Thread::current();
  assert(thread->is_Java_thread(), "Must be JavaThread");
  JavaThread *jt = (JavaThread *)thread;

  // 判断线程是否已经被中断
  if (Thread::is_interrupted(thread, false)) {
    return;
  }

  // Next, demultiplex/decode time arguments
  timespec absTime;
  // park方法的传参是isAbsolute = false, time = 0,所以会继续往下走
  if (time < 0 || (isAbsolute && time == 0) ) { // don't wait at all
    return;
  }
  // 这里time为0,如果调用的是parkNanos或者parkUtil,这里time就会>0,
  if (time > 0) {
    // 如果time > 0,unpackTime计算absTime的时间
    unpackTime(&absTime, isAbsolute, time);
  }

  ThreadBlockInVM tbivm(jt);

  // 再次判断线程是否被中断,如果没有被中断,尝试获得互斥锁,如果获取失败,直接返回
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
    return;
  }

  int status ;
  // 如果_counter > 0, 不需要等待,这里再次检查_counter的值
  if (_counter > 0)  { // no wait needed
    _counter = 0;
    status = pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
    // 插入一个写内存屏障,保证可见性,具体实现见下方
    OrderAccess::fence();
    return;
  }

// 省略assert代码

  OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);
  // 设置JavaThread的_suspend_equivalent为true,表示线程被暂停
  jt->set_suspend_equivalent();
  // cleared by handle_special_suspend_equivalent_condition() or java_suspend_self()

  assert(_cur_index == -1, "invariant");
  if (time == 0) {
    _cur_index = REL_INDEX; // arbitrary choice when not timed
    // 让线程等待_cond[_cur_index]信号,到这里线程进入等待状态
    status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
  } else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    // 线程进入有超时时间的等待,内部实现调用了pthread_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代码
 // _counter重新设置为0
  _counter = 0 ;
  // 释放互斥锁
  status = pthread_mutex_unlock(_mutex) ;
  assert_status(status == 0, status, "invariant") ;
  // 插入写屏障
  OrderAccess::fence();

  // 省略额外检查代码
}

// OrderAccess::fence 在linux平台的实现
inline void OrderAccess::fence() {
  // 如果是多核cpu
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
    // 判断是否AMD64 CPU,汇编代码实现写屏障
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}

ThreadBlockInVM tbivm(jt) (就是阻塞)

这属于C++新建变量的语法,也就是调用构造函数新建了一个变量,变量名为tbivm,参数为jt。类的实现为

class ThreadBlockInVM : public ThreadStateTransition {
 public:
  ThreadBlockInVM(JavaThread *thread)
  : ThreadStateTransition(thread) {
    // Once we are blocked vm expects stack to be walkable    
    thread->frame_anchor()->make_walkable(thread);
   //把线程由运行状态转成阻塞状态
    trans_and_fence(_thread_in_vm, _thread_blocked);
  }
  ...
};

_thread_in_vm 表示线程当前在VM中执行,_thread_blocked表示线程当前阻塞了,他们globalDefinitions.hpp中定义的枚举

//这个枚举是用来追踪线程在代码的那一块执行,用来给 safepoint code使用,有4种重要的类型,_thread_new/_thread_in_native/_thread_in_vm/_thread_in_Java。形如xxx_trans的状态都是中间状态,表示线程正在由一种状态变成另一种状态,这种方式使得 safepoint code在处理线程状态时,不需要对线程进行挂起,使得safe point code运行更快,而给定一个状态,通过+1就可以得到他的转换状态
enum JavaThreadState {
  _thread_uninitialized     =  0, // should never happen (missing initialization) 
_thread_new               =  2, // just starting up, i.e., in process of being initialized 
_thread_new_trans         =  3, // corresponding transition state (not used, included for completeness)  
_thread_in_native         =  4, // running in native code  . This is a safepoint region, since all oops will be in jobject handles
_thread_in_native_trans   =  5, // corresponding transition state  
_thread_in_vm             =  6, // running in VM 
_thread_in_vm_trans       =  7, // corresponding transition state 
_thread_in_Java           =  8, //  Executing either interpreted or compiled Java code running in Java or in stub code  
_thread_in_Java_trans     =  9, // corresponding transition state (not used, included for completeness) 
_thread_blocked           = 10, // blocked in vm 
_thread_blocked_trans     = 11, // corresponding transition state 
_thread_max_state         = 12  // maximum thread state+1 - used for statistics allocation
};

父类ThreadStateTransition中定义trans_and_fence如下

void trans_and_fence(JavaThreadState from, JavaThreadState to) { transition_and_fence(_thread, from, to);} //_thread即构造函数传进来de thread
// transition_and_fence must be used on any thread state transition
// where there might not be a Java call stub on the stack, in
// particular on Windows where the Structured Exception Handler is
// set up in the call stub. os::write_memory_serialize_page() can
// fault and we can't recover from it on Windows without a SEH in
// place.
//transition_and_fence方法必须在任何线程状态转换的时候使用
static inline void transition_and_fence(JavaThread *thread, JavaThreadState from, JavaThreadState to) {
  assert(thread->thread_state() == from, "coming from wrong thread state");
  assert((from & 1) == 0 && (to & 1) == 0, "odd numbers are transitions states");
//标识线程转换中
    thread->set_thread_state((JavaThreadState)(from + 1));

  // 设置内存屏障,确保新的状态能够被VM 线程看到
if (os::is_MP()) {
    if (UseMembar) {
      // Force a fence between the write above and read below     
        OrderAccess::fence();
    } else {
      // Must use this rather than serialization page in particular on Windows      
        InterfaceSupport::serialize_memory(thread);
    }
  }

  if (SafepointSynchronize::do_call_back()) {
    SafepointSynchronize::block(thread);
  }
//线程状态转换成最终的状态,对待这里的场景就是阻塞
  thread->set_thread_state(to);

  CHECK_UNHANDLED_OOPS_ONLY(thread->clear_unhandled_oops();)
}

https://juejin.im/post/5c041bae51882516eb562f15#heading-7

unpark

void Parker::unpark() {
  int s, status ;
  // 先进入_mutex的临界区,声明如下
  // pthread_mutex_t _mutex [1] ;
  // pthread_cond_t  _cond  [2] ; 
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant") ;
  s = _counter;
  // 将_counter置为1
  _counter = 1;
  // s记录的是unpark之前的_counter数,如果s < 1,说明有可能该线程在等待状态,需要唤醒。
  if (s < 1) {
    // thread might be parked
    // _cur_index代表被使用cond的index
    if (_cur_index != -1) {
      // thread is definitely parked
      // 根据虚拟机参数WorkAroundNPTLTimedWaitHang来做不同的处理,默认该参数是1
      if (WorkAroundNPTLTimedWaitHang) {
        // 先唤醒目标等待的线程
        status = pthread_cond_signal (&_cond[_cur_index]);
        assert (status == 0, "invariant");
        // 释放互斥锁,先唤醒后释放锁,可能会导致线程被唤醒后获取不到锁,再次进入等待状态,我的理解是效率可能会低一丢丢
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant");
      } else {
        // 先释放锁
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant");
        // 后发信号唤醒线程,唤醒操作在互斥代码块外部,感觉这里可能会有风险,暂时还GET不到。。。
        status = pthread_cond_signal (&_cond[_cur_index]);
        assert (status == 0, "invariant");
      }
    } else {
      // 如果线程没有在等待,直接返回
      pthread_mutex_unlock(_mutex);
      assert (status == 0, "invariant") ;
    }
  } else {
    // 如果线程没有在等待,直接返回
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
  }
}

https://juejin.im/post/5d72f75d5188255aed032abb#heading-5

简要

park 过程

当调用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;

如果有中断信号,就直接返回

if (Thread::is_interrupted(thread, false)) {
 // 线程执行了中断,返回
    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并返回:
ps:这里可能顺序不对,应该在阻塞的上面
完整源码
https://juejin.im/post/5c041bae51882516eb562f15#heading-2
https://juejin.im/post/5d72f75d5188255aed032abb#heading-2

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 过程

当unpark时,则简单多了,直接设置_counter为1,再unlock mutex返回。如果_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") ;  
  }  
}

链接:https://www.jianshu.com/p/e3afe8ab8364

源码总结:

park

1:直接返回
interrupt:直接返回
时间到了:直接返回
否则: 阻塞

unpark

unpark之前的值
0:唤醒阻塞的线程
1:直接返回

unpark立即唤醒park中的阻塞

源码博文

https://juejin.im/post/5c041bae51882516eb562f15#heading-2(推荐)
https://juejin.im/post/5d72f75d5188255aed032abb#heading-2(推荐)
https://www.jianshu.com/p/e3afe8ab8364(简单)

只要记住

park

1:不阻塞
interrupt:不阻塞
否则: 阻塞

unpark

unpark之前的值
0:唤醒
1:无

unpark立即唤醒park中的阻塞

再简洁(最终):

park遇到0 阻塞 (park不能减1阻塞)
unpark之前的是0 唤醒(unpark可以加1唤醒)

park会被interrupt唤醒
unpark不能连续+1,不能叠加。

unpark立即唤醒park中的阻塞

发布了540 篇原创文章 · 获赞 3129 · 访问量 253万+

猜你喜欢

转载自blog.csdn.net/dataiyangu/article/details/104932306