Deep understanding of the synchronized underlying source code is enough for beginners

Keywords: jvm Java JDK

Deep understanding of synchronized underlying source code

preface

This article analyzes the implementation logic of synchronized from the JVM source code, so as to better understand the depth of synchronized.

Process: the basic unit of operating system resource allocation. Thread: the basic unit of cpu scheduling (real execution)

Reading navigation

1, Use scenarios of synchronized

synchronized is generally used in the following scenarios:

  1. Decorate code block, specify a locked object, lock the object

    public Demo1{
       Object lock=new Object();
       public void test1(){
           synchronized(lock){
           }
       }
    }
    
  2. Modify the static method to lock the Class object of the current Class

    public class Demo2 {
       //Form 1
        public void test1(){
            synchronized(Synchronized.class){
            }
        }
      //Form 2
        public void test2(){
            public synchronized static void test1(){
            }
        }
    }
    
  3. Modify the common method to lock the current instance object this

    public class Demo3 {
        public synchronized void test1(){
        }
    }
    

2, Layout of objects in memory in JVM

The lock implemented by synchronized is stored in the Java object header. So to have a deep understanding of synchronized, first of all, what is the layout of objects in memory?

In the JVM, objects are divided into three areas in memory: object header, instance data, and padding.

1. The object header consists of two parts: markOop or Mark Word and class meta information

Mark Word: store the HashCode, generation age and lock flag information of the object by default. It will reuse its own storage space according to the state of the object, that is to say, the data stored in Mark Word will change with the change of lock flag bit during running. The source code is as follows:

class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }
 public:
  // Constants
  enum { age_bits                 = 4,  //Generational age
         lock_bits                = 2, //Lock identification
         biased_lock_bits         = 1, //Biased lock or not
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits, //Object's hashcode
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2 //Timestamp of biased lock
  };

Class meta information: the pointer of the object to its class metadata. The virtual machine uses this pointer to determine which class instance this object is.

2. Instance data: this part mainly stores the data information of the class and the information of the parent class

3. Fill it

3, synchronized bottom layer starts from bytecode

First, let's take a look at the small Demo case of the method and method block decorated with synchronized, and then analyze it. Since the implementation of synchronized is at the JVM level, we need to understand it in depth, that is, starting from bytecode.

public class Demo03 {
    public static void main( String[] args ){
        System.out.println( "hello Java" );
    }
    //synchronized to modify common methods
    public synchronized void test1() { }

    //Decorated code block
    public void test2() {
        synchronized (this) {}
    }
}

Next, open the terminal to run javac Demo03.java, which means to change Demo03.java to Demo03.class, and then javap -v Demo03.class, so you can check the bytecode as follows.

If you look at the bytecode above, you will find

When synchronizing method:

If the modified synchronization method is through flag ACC_SYNCHRONIZED to complete, that is, once this method is implemented, it will first determine whether there is a flag bit, and then ACC_SYNCHRONIZED implicitly calls the two instructions just now: monitorenter and monitorexit.

When synchronized decorates a synchronized block:

First of all, if it is decorated in the method block by synchronized, the execution right of the thread is obtained through the two bytecode instructions of monitorenter and monitorexit. The lock will be released automatically after the method exits or in case of exception.

No matter which of the above is decorated: no matter which essence is to obtain an object monitor

When the Java virtual machine executes the monitorcenter instruction, 1 A kind of Firstly, it will try to acquire the lock of the object. If the object has no lock, or the current thread already has the lock of the object, it will increase the counter by + 1; then when the monitorexit instruction is executed, it will increase the counter by - 1; then when the counter is 0, the lock will be released. Two A kind of If the lock acquisition fails, the current thread will block and wait until the object lock is released by another thread.

4, What is monitor?

Monitor it is a monitor, the underlying source code is written in C + +. In the hotspot virtual machine, it uses the ObjectMonitor class to implement the monitor source code as follows:

bool has_monitor() const {
    return ((value() & monitor_value) != 0);
  }
  ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
  }

monitor is implemented in virtual machine ObjectMonitor.hpp In the document:

 class ObjectMonitor {
...
  ObjectMonitor() {
    _header       = NULL; //markOop object header
    _count        = 0;    
    _waiters      = 0,   //Number of waiting threads
    _recursions   = 0;   //Thread reentry times
    _object       = NULL;  //Store Monitor object
    _owner        = NULL;  //Thread to get ObjectMonitor object
    _WaitSet      = NULL;  //List of threads in wait state
    _WaitSetLock  = 0 ; 
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;	// One way list
    FreeNext      = NULL ;
    _EntryList    = NULL ; //Thread waiting for lock BLOCKED
    _SpinFreq     = 0 ;   
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ; 
    _previous_owner_tid = 0; //ID of the previous owning thread of the monitor
  }
...

5, Go deep into the synchronized underlying source code

Start reading the source code from the two instructions of monitorenter and monitorexit. After the JVM loads the bytecode into memory, it will interpret and execute the two instructions. The instructions of monitorenter and monitorexit are parsed through InterpreterRuntime.cpp Two methods of.

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {//Bypass the biased lock and enter the lightweight lock directly
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END


//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (elem == NULL || h_obj()->is_unlocked()) {
    THROW(vmSymbols::java_lang_IllegalMonitorStateException());
  }
  ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
  // Free entry. This must be done here, since a pending exception might be installed on
  // exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
  elem->set_obj(NULL);
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

Note:

//JavaThread current thread to acquire lock
 //BasicObjectLock

UseBiasedLocking is the identification of whether to start the biased lock when the JVM starts
1. If biased locks are supported, objectsynchronizer:: fast is executed_ The logic of enter

ObjectSynchronizer::fast_enter is implemented in synchronizer.cpp In the file, the code is as follows

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 if (UseBiasedLocking) {//Judge whether to open the lock
    if (!SafepointSynchronize::is_at_safepoint()) {//If not at a global security point
      //Via ` revoke_ and_ This function attempts to acquire the biased lock
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {//If it is cancellation and heavy bias direct return
        return;
      }
    } else {//If at a safe point, remove the biased lock
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }

 slow_enter (obj, lock, THREAD) ;
}

Summary:

  • Check again that the deflection lock is open

  • When at an unsafe point, by revoke_and_rebias attempts to acquire the biased lock. If it succeeds, it returns directly. If it fails, it enters the lightweight lock acquisition process

  • revoke_ and_ The acquisition logic of the biased lock in rebias biasedLocking.cpp in

  • If the deflection lock is not opened, enter slow_ The process of getting lightweight lock by enter

2. If biased locks are not supported, objectsynchronizer:: slow is executed_ The enter logic bypasses the biased lock and directly enters the lightweight lock, which is also located in the synchronizer.cpp In file

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");

  if (mark->is_neutral()) {//If the current status is unlocked, mark word's
    //Save mark directly to the_ displaced_header field
    lock->set_displaced_header(mark);
    //Through CAS, mark word is updated to a pointer to the BasicLock object. A successful update indicates that a lightweight lock has been obtained
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } 
  //If markword is locked and the ptr pointer in markword points to the stack frame of the current thread, it means a re-entry operation, and there is no need to scramble for the lock
  else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    lock->set_displaced_header(NULL);
    return;
  }

#if 0
  // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif
	//When the code is executed here, it indicates that there are multiple threads competing for lightweight locks. Lightweight locks are upgraded to heavyweight locks through 'inflate'
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

6, Lock optimization

Everyone knows that before JDK 1.6, synchronized was a heavyweight lock with low efficiency. So the official started in JDK 1.6, in order to reduce the performance consumption caused by obtaining and releasing locks, we optimized synchronized and introduced the concepts of biased lock and lightweight lock.

There are four states: unlocked state > biased lock state > lightweight lock state > heavyweight lock state. The lock state will gradually upgrade with the lock competition, and the lock upgrade is irreversible.

1. Biased lock

Biased lock refers to that if a thread obtains a biased lock, if there are no other threads competing for the lock in the next period of time, the thread holding the biased lock will enter the synchronization again, and it is not necessary to preempt the lock and release the lock again.

This process uses CAS objective lock operation, which means that when a thread acquires biased lock, if there are no other threads competing for the lock, then the thread holding biased lock will enter again, and the virtual machine will not conduct any synchronous operation, just add 1 to the flag bit; if different threads compete for the lock, CAS will fail, so acquiring the lock will fail.

JDK 1.5 biased lock is closed. The opening parameter xx:-UseBiasedLocking=false. It is opened by default after JDK 1.6.

2. Acquisition of biased lock

When a thread accesses the synchronization block to acquire a lock, it will store the thread ID of the biased lock in the lock record in the object header (Mark Word) and stack frame, indicating which thread obtains the biased lock.

Acquisition process:

1) First, judge whether it is in the state of biased lock according to the mark of lock

2) If it is in the biased lock state, write its thread ID to MarkWord through CAS operation. If CAS operation is successful, it means that the current thread obtains the biased lock, and then continues to execute the synchronization code block. If CAS fails, it means that acquiring lock fails.

3) If it is not biased lock at present, it will detect whether the thread ID stored in MarkWord is equal to the thread ID of the currently accessed thread. If it is equal, it means that the current thread has acquired biased lock, and then directly execute synchronization code; if it is not equal, it means that the current biased lock is acquired by other threads, and the biased lock needs to be revoked.

3. Undo biased lock

The thread that obtains the biased lock will release the biased lock, and the process of revoking the biased lock needs to wait for a global security point (that is, the thread that waits for the biased lock to stop the bytecode execution).

The process of undoing the biased lock:

1) First of all, judge whether the thread obtaining the biased lock is alive

2) If the thread is dead, set Mark Word to unlocked state

3) If the thread is still alive, when the global security point is reached, the thread obtaining the biased lock will be suspended, then the biased lock will be upgraded to a lightweight lock, and finally the thread blocked at the global security point will be awakened to continue to execute the synchronization code

4. Lightweight lock

When multiple threads compete for biased locks, biased lock revocation occurs. Biased lock revocation has two states:

1) Unlocked state without acquiring biased lock

2) Lock state without acquiring biased lock

5. Lightweight lock process

1) If the object is unlocked, the JVM will create a lockrecord in the stack frame of the current thread to copy the Mark Word in the object header to the lock record

2) The JVM then uses CAS to replace Mark Word in the object header with a pointer to the lock record

3) If the replacement is successful, it means that the current thread obtains the lightweight lock; if the replacement fails, it means that there are other threads competing for the lock. Then the current thread will try to use CAS to acquire the lock. When the spin exceeds a specified number of times (it can be customized), the lock will expand and upgrade to a heavyweight lock

Spin to prevent the thread from being suspended. Once the resource can be obtained, the direct attempt is successful. If the threshold value is exceeded and the lock has not been acquired, then it will be upgraded to a heavyweight lock. (spin lock is 10 times by default, - XX: PreBlockSpin can be modified)

6. Heavyweight lock status

Once the lock is upgraded to a heavyweight lock, it will not be restored to a lightweight lock state. When the lock is in the heavyweight lock state, other threads will be BLOCKED when trying to acquire the lock, that is, BLOCKED state. When the thread holding the lock releases the lock, it will wake up these sites, and the thread after being awakened will have a new round of competition.

reference material

Aobing: Knock on the bottom implementation of synchronized

Architect's Training Manual: [5) Synchronized principle analysis](

Posted by azaidi7 on Thu, 25 Jun 2020 21:55:14 -0700