Source code analysis of LockSupport(park&unpark)

Keywords: Java source code

Source code analysis of LockSupport(park/unpark)

park method

park()

public static void park() {
    UNSAFE.park(false, 0L);
}

park(Object blocker)

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

blocker is used to record who blocks a thread when it is blocked. Used for thread monitoring and analysis tools to locate the cause.

The setBlocker(t, blocker) method is used to record that the t thread is blocked by the broker.

Therefore, we only focus on the core method, which is UNSAFE.park(false, 0L).

    //park
    public native void park(boolean isAbsolute, long time);
    
    //unpack
    public native void unpark(Object var1);

UNSAFE is a very powerful class. Its operation is based on the bottom layer, that is, it can directly operate memory,

Underlying implementation principle of Unsafe.park

Under Linux system, it is implemented by mutex and condition in Posix thread library pthread.
mutex and condition protect a_ The variable of counter is set to 0 when it is park and 1 when it is unpark.

_ The counter field is used to record the so-called "license"

The condition variable is used to block a thread. When the condition is not satisfied, the thread often unlocks the corresponding mutex and waits for the condition to change. Once another thread changes the condition variable, it will notify the corresponding condition variable to wake up one or more threads blocked by this condition variable. These threads will re lock the mutex and retest whether the condition is met.

Each Java thread has a Parker instance. The Parker class is defined as follows:

park.hpp

/* In the future we'll want to think about eliminating Parker and using
 * ParkEvent instead.  There's considerable duplication between the two
 * services.
 */
/*
*In the future, we will consider getting rid of Parker and replacing it with ParkEvent. There are quite a lot of duplicate services between the two.
*/
class Parker : public os::PlatformParker {
private:
  volatile int _counter ;
  Parker * FreeNext ;
  JavaThread * AssociatedWith ; // Current association

public:
  Parker() : PlatformParker() {
    _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.
  void park(bool isAbsolute, jlong time);
  void unpark();

  // Lifecycle operators
  static Parker * Allocate (JavaThread * t) ;
  static void Release (Parker * e) ;
private:
  static Parker * volatile FreeList ;
  static volatile int ListLock ;

};

os::PlatformParker is implemented by the operating system, such as linux

os_linux.hpp

class PlatformParker : public CHeapObj<mtInternal> {
  protected:
    enum {
        REL_INDEX = 0,
        ABS_INDEX = 1
    };
    int _cur_index;  // which cond is in use: -1, 0, 1  
    pthread_mutex_t _mutex [1] ;
    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;
      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; // mark as unused
    }
};

① When calling park, first try to get the "license" directly, that is_ When counter > 0, if successful, the_ Set counter to 0 and return.

② If not, construct a ThreadBlockInVM and check_ Is counter > 0? If yes, set_ Set counter to 0, unlock mutex and return:

③ Otherwise, judge the waiting time, and then call pthread_ cond_ The wait function waits. If the wait returns, the_ Set counter to 0, unlock mutex and return:

os_linux.cpp

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.
    //① First try whether you can get the "license" directly
  if (Atomic::xchg(0, &_counter) > 0) return;

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

  // Optional optimization -- avoid state transitions if there's an interrupt pending.
  // Check interrupt before trying to wait
  if (Thread::is_interrupted(thread, false)) {
    return;//If interrupted by interrupt, wake up and return
  }

  // Next, demultiplex/decode time arguments
  timespec absTime;
  if (time < 0 || (isAbsolute && time == 0) ) { // don't wait at all
      //When the time is up, or represents the absolute time. At the same time, the absolute time is 0 (it is also time at this time). It is returned directly. The parkUtil in java transmits the absolute time, and others are not
    return;
  }
  if (time > 0) {
      //The time parameter is passed in, stored in absTime, and parsed into absTime - > TV_ SEC and absTime - > TV_ Nsec (nanosecond) stores absolute time
    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.
    //② Construct a ThreadBlockInVM and check it_ Is counter > 0? If yes, set_ Set counter to 0, unlock mutex and return:
 //Enter the safepoint region and change the thread to the blocking state
  ThreadBlockInVM tbivm(jt);

  // Don't wait if cannot get lock since interference arises from
  // unblocking.  Also. check interrupt before trying wait
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
       //If the thread is interrupted or fails to lock the mutex variable in the process of trying to lock it, for example, it is locked by other threads, it returns directly
    return;
  }

//This indicates that the thread mutex locking is successful
  int status ;
    //The license is 1 and no waiting is required
  if (_counter > 0)  { // no wait needed
    _counter = 0;//There is already a license. Use the current license
      //Unlock mutually exclusive variables
    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.
      //Use memory barriers to ensure_ If counter is set to 0 (write operation), the previous result of the memory barrier can be obtained by the read operation after the memory barrier, that is, 0 can be read correctly
    OrderAccess::fence();
    return;//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

//Set the operating system thread owned by the java thread to CONDVAR_WAIT status, indicating that you are waiting for a condition to occur
  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");
  if (time == 0) {
    _cur_index = REL_INDEX; // arbitrary choice when not timed
      //③ Determine the waiting time, and then call pthread_ cond_ The wait function waits. If the wait returns, the_ Set counter to 0, unlock mutex and return:
      //Put the calling thread on the thread list of the waiting condition, and then unlock the mutex variable (these two are atomic operations). At this time, the thread enters the waiting. When it returns, the mutex variable is locked again.
  //0 is returned successfully, otherwise the error number is returned
    status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
  } else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    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
//After waiting, the license is consumed and changed to 0_ counter = 0 ;
//Release lock of mutex
  _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.
    
//Add memory barrier instruction
  OrderAccess::fence();

  // If externally suspended while waiting, re-suspend
  if (jt->handle_special_suspend_equivalent_condition()) {
    jt->java_suspend_self();
  }
}

ThreadBlockInVM tbivm(jt)
This belongs to the syntax of creating a new variable in C + +, that is, calling the constructor to create a new variable, the variable name is tbivm, and the parameter is jt. Class is implemented as

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);
   //Turn the thread from running state to blocking state
    trans_and_fence(_thread_in_vm, _thread_blocked);
  }
  ...
};

_ thread_in_vm indicates that the thread is currently executing in VM_ thread_blocked indicates that the thread is currently blocked. They are enumerations defined in globalDefinitions.hpp

//This enumeration is used to track the execution of threads in the code. It is used for safepoint code. There are four important types_ thread_new/_thread_in_native/_thread_in_vm/_thread_in_Java.  Shaped like xxx_ The trans states are all intermediate states, indicating that the thread is changing from one state to another. This way makes the safepoint code do not need to suspend the thread when processing the thread state, making the safe point code run faster. Given a state, its transition state can be obtained by + 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
};

This is the process of the whole park. To sum up, it is the process of consuming "licenses".

unpark

  /**
     * If the license for the given thread is not yet available, make it available.
     * If a thread is blocked on a park, it will unblock it.
     * Otherwise, ensure that the next call to park will not be blocked.
     * If the given thread has not been started, there is no guarantee that this operation will have any effect. 
     * @param thread: The thread to perform the unpark operation; A null parameter indicates that this operation has no effect.
     */
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

Unsafe.class

public native void unpark(Object var1);

Underlying implementation principle of Unsafe.unpark

When unpark is used, it is much simpler and can be set directly_ Counter is 1, and then unlock mutext returns. If_ If the value before counter is 0, pthread will also be called_ cond_ Signal wakes up the thread waiting in the park:

os_linux.cpp

void Parker::unpark() {
  int s, status ;
    //Lock the mutex. If the mutex is locked, it will be blocked until the mutex is unlocked
//When park enters the wait_ mutex will be released
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant") ;
  s = _counter;Store old_counter permit
  _counter = 1;//Set license to 1
  if (s < 1) {//s=0 blocked by park
    // thread might be parked
    if (_cur_index != -1) {
      // thread is definitely parked
      if (WorkAroundNPTLTimedWaitHang) {
        status = pthread_cond_signal (&_cond[_cur_index]);//Wake up the thread waiting in the park
        assert (status == 0, "invariant");
        status = pthread_mutex_unlock(_mutex);//Release lock
        assert (status == 0, "invariant");
      } else {
        status = pthread_mutex_unlock(_mutex);//Release lock
        assert (status == 0, "invariant");
        status = pthread_cond_signal (&_cond[_cur_index]);//Wake up the thread waiting in the park
        assert (status == 0, "invariant");
      }
    } else {
      pthread_mutex_unlock(_mutex);//Release lock
      assert (status == 0, "invariant") ;
    }
  } else {
      //If you always have permission, release the lock you added, and if you have permission, the park itself will return
    pthread_mutex_unlock(_mutex);//Release lock
    assert (status == 0, "invariant") ;
  }
}

The function of unpark itself is to issue licenses and notify the waiting thread that the waiting can be ended

Source code interpretation

Make an analogy
Thread is like a traveler, Parker is like his backpack_ cond condition variable is like a tent in a backpack, providing a place to rest_ counter is like the spare dry food in the backpack (0 is exhausted and 1 is sufficient)
Call park to see if you need to stop and rest
If the spare dry food runs out, get into the tent and rest
If there is enough spare dry food, don't stop and move on
Calling unpark is like replenishing dry food to make it sufficient
If the thread is still in the tent, wake up and let him move on
If the thread is still running at this time, the next time he calls park, he will only consume the spare dry food without staying and moving on
Because the backpack space is limited, calling unpark multiple times will only supplement one spare dry food

Call park()

  1. The current thread calls the Unsafe.park() method

  2. Check_ counter, if the case is 0, then the_ Mutex mutex

  3. Thread entry_ cond condition variable, enter the blocking state and release the mutex

  4. To be on the safe side, set it again_ counter = 0

Call unpark()

  1. Call the Unsafe.unpark(Thread_0) method to set_ counter is 1

  2. Awaken_ Thread in cond condition variable_ 0

  3. Thread_0 resume operation

  4. Consumer license, setting_ counter is 0

First call unpark(), then call park().

  1. Call the Unsafe.unpark(Thread_0) method to set_ counter is 1

  2. The current thread calls the Unsafe.park() method

  3. Check_ counter, this case is 1. At this time, the thread does not need to be blocked and continues to run

  4. Set_ counter is 0

Posted by iamchris on Mon, 20 Sep 2021 20:04:38 -0700