15,000 words, 6 code cases, and 5 schematic diagrams allow you to thoroughly understand Synchronized

Synchronized

This article will focus on the synchronized keyword and use a large number of pictures and cases to describe the implementation of CAS, synchronized Java level and C++ level, the principle of lock upgrade, source code, etc.

Approximate viewing time: 17 minutes

You can read this article with a few questions. If you read it carefully, the problems will be solved:

1. How is synchronized used? How is it implemented at the Java level?

2. What is CAS? What benefits can it bring? What are the disadvantages?

3. What is the mark word? What does it have to do with synchronized?

4. What is the lock upgrade optimization of synchronized? How to implement it at C++ level?

5. Will the lightweight lock CAS in JDK 8 spin if it fails?

6. What is an object monitor? How is the wait/notify method implemented? When using synchronized, how are threads sorted in the blocking queue after blocking?

...

synchronized Java level implementation

synchronized acts on code blocks or methods to ensure the synchronization mechanism in a concurrent environment.

Any thread that encounters synchronized must first acquire the lock before performing operations in the code block or method.

In Java, each object has a corresponding monitor object (monitor). When the lock of object A is acquired, a field in the monitor object of object A will point to the current thread, indicating that this thread has acquired the lock of object A. (Detailed principles are described later)

synchronized can be used on ordinary objects and static objects. When used on static objects and static methods, it will obtain the lock of its corresponding Class object.

When synchronized acts on a code block, monitorentry and monitorexit bytecode instructions will be used to identify locking and unlocking.

When synchronized is applied to a method, synchronized will be added to the access identifier.

There may be two monitorexit instructions in the instruction because when an exception occurs, monitorexit will be automatically executed to unlock it.

The normal process is PC 12-14. If an exception occurs during this period, it will jump to PC 17, and finally execute monitorexit on PC 19 to unlock.

        Object obj = new Object();
        synchronized (obj) {
​
        }

image.png

In the previous article we talked about atomicity, visibility and orderliness

The synchronized lock and unlock bytecode instructions use barriers. When locking, the shared memory is re-read from the main memory. Before unlocking, the working memory data is written back to the main memory to ensure visibility.

Since the execution after acquiring the lock is equivalent to serial execution, atomicity and orderliness are guaranteed. It should be noted that the instructions between locking and unlocking can still be reordered.

CAS

In order to better explain the synchronized principle and lock upgrade, let's first talk about CAS

As we said in the previous article , volatile cannot guarantee the atomicity of composite operations. Using the synchronized method or CAS can ensure the atomicity of composite operations.

So what is CAS?

The full name of CAS is Compare And Swap. When you want to modify the data after reading it, compare the read data with the value at the address. If they are equal, replace the value at the address with the target value. If they are not equal, it will usually be reset. Read the data and then perform the CAS operation, which means retrying after failure.

Synchronized locking is a pessimistic strategy. Every time you encounter it, you think there will be a concurrency problem, and you must first acquire the lock before operating.

CAS is an optimistic strategy. Every time you operate boldly, if the operation fails (CAS failure), you will then use compensatory measures (retry after failure).

The combination of CAS and failed retry (loop) constitutes optimistic locking or spin lock (loop attempts are very similar to self-rotation)

The atomic classes under the concurrent package rely on Unsafe to use a large number of CAS operations, such as the auto-increment of AtomicInteger.

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
​
    //var1是调用方法的对象,var2是需要读取/修改的值在这个对象上的偏移量,var4是自增1
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //var5是通过对象和字段偏移量获取到字段最新值
            var5 = this.getIntVolatile(var1, var2);
            //cas:var1,var2找到字段的值 与 var5比较,相等就替换为 var5+var4 
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

CAS can only operate on one variable. If you want to operate on multiple variables, you can only encapsulate one layer (encapsulate multiple variables as fields of a new object), and then use theAtomicReference

I wonder if any of you have discovered that there is a bug in the CAS process, that is, between reading data and comparing data, if the data is changed from A to B, and then to A, then CAS can also be executed successfully.

Some businesses can accept this scenario, while others cannot. This is the so-called ABA problem.

The way to solve the ABA problem is relatively simple. You can append an auto-incremented version number when comparing. JDK also provides atomic classes to solve the ABA problem.AtomicStampedReference

CAS can avoid thread blocking, but if it continues to fail, it will continue to loop, increasing CPU overhead. The number/duration of retries after CAS fails is difficult to evaluate.

Therefore, the CAS operation is suitable for scenarios with low competition. The overhead of CPU idling is exchanged for the overhead of thread blocking, suspension/resumption.

lock upgrade

Early versions of synchronized will directly suspend threads that cannot obtain the lock, resulting in poor performance.

The implementation of synchronized is optimized in JDK 6, that is, lock upgrade

The lock status can be divided into no lock, biased lock, lightweight lock, and heavyweight lock.

We can temporarily understand heavyweight locks as letting the thread hang if the lock cannot be obtained early. The new optimization is lightweight locks and biased locks.

mark word

In order to better explain the lock upgrade, let's first talk about the mark word in the Java object header.

Our following explorations are all about 64-bit virtual machines.

The memory of a Java object consists of mark word, klass word, if it is an array, record the length, instance data (field), and fill it (fill it to a multiple of 8 bytes)

mark word will record the lock status, and the recorded data will be different under different lock statuses.

The following table shows the contents of mark word records from no lock to heavyweight lock.

|----------------------------------------------------------------------|--------|--------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1  | lock:2 | 无锁   
|----------------------------------------------------------------------|--------|--------|
|  thread:54 |         epoch:2      | unused:1 | age:4 | biased_lock:1 | lock:2 | 偏向锁
|----------------------------------------------------------------------|--------|--------|
|                     ptr_to_lock_record:62                            | lock:2 | 轻量级锁
|----------------------------------------------------------------------|--------|--------|
|                     ptr_to_heavyweight_monitor:62                    | lock:2 | 重量级锁
|----------------------------------------------------------------------|--------|--------|

unused means not used yet

identity_hashcode is used to record consistent hashing

age is used to record GC age

biased_lock identifies whether to use biased lock, 0 means not enabled, 1 means enabled

lock is used to identify the lock status flag, 01 no lock or biased lock, 00 lightweight lock, 10 heavyweight lock

thread is used to identify the biased thread

epoch record bias timestamp

ptr_to_lock_record records the lock record in the stack frame (described later)

ptr_to_heavyweight_monitor records the thread that acquires the heavyweight lock

jolView mark word

Students who are more familiar with mark word can skip it.

After understanding the mark word, let’s get familiar with the mark word in different lock states. I use jol to view the memory.

       <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.12</version>
        </dependency>
no lock

The mark word used by students during the experiment may be different from the one in my comments. We mainly check the value of the lock identifier and whether the biased lock is enabled.

image.png

    public void noLock() {
        Object obj = new Object();
        //mark word  00000001 被unused:1,age:4,biased_lock:1,lock:2使用,001表示0未启用偏向锁,01表示无锁
        //01 00 00 00  (00000001 00000000 00000000 00000000)
        //00 00 00 00  (00000000 00000000 00000000 00000000)
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        System.out.println(objClassLayout.toPrintable());
​
        //计算一致性哈希后
        //01 b6 ce a8
        //6a 00 00 00
        obj.hashCode();
        System.out.println(objClassLayout.toPrintable());
​
        //进行GC 查看GC年龄 0 0001 0 01 前2位表示锁状态01无锁,第三位biased_lock为0表示未启用偏向锁,后续四位则是GC年龄age 1
        //09 b6 ce a8 (00001001 10110110 11001110 10101000)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.gc();
        System.out.println(objClassLayout.toPrintable());
    }
lightweight lock
    public void lightLockTest() throws InterruptedException {
        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        //1334729950
        System.out.println(obj.hashCode());
        //0 01 无锁
        //01 4e c0 d5 (00000001 01001110 11000000 11010101)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
​
​
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                // 110110 00 中的00表示轻量级锁其他62位指向拥有锁的线程
                //d8 f1 5f 1d (11011000 11110001 01011111 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
​
                //1334729950
                //无锁升级成轻量级锁后 hashcode未变 对象头中没存储hashcode 只存储拥有锁的线程
                //(实际上mark word内容被存储到lock record中,所以hashcode也被存储到lock record中)
                System.out.println(obj.hashCode());
            }
        }, "t1");
​
        thread1.start();
        //等待t1执行完 避免 发生竞争
        thread1.join();
​
        //轻量级锁 释放后 mark word 恢复成无锁 存储哈希code的状态
        //01 4e c0 d5 (00000001 01001110 11000000 11010101)
        //6a 00 00 00 (01101010 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
​
        Thread thread2 = new Thread(() -> {
            synchronized (obj) {
                //001010 00 中的00表示轻量级锁其他62位指向拥有锁的线程
                //28 f6 5f 1d (00101000 11110110 01011111 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t2");
        thread2.start();
        thread2.join();
    }
bias lock
    public void biasedLockTest() throws InterruptedException {
        //延迟让偏向锁启动
        Thread.sleep(5000);
​
        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
​
        //1 01 匿名偏向锁 还未设置偏向线程
        //05 00 00 00 (00000101 00000000 00000000 00000000)
        //00 00 00 00 (00000000 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
​
        synchronized (obj) {
            //偏向锁 记录 线程地址
            //05 30 e3 02 (00000101 00110000 11100011 00000010)
            //00 00 00 00 (00000000 00000000 00000000 00000000)
            System.out.println(Thread.currentThread().getName() + ":");
            System.out.println(objClassLayout.toPrintable());
        }
​
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                //膨胀为轻量级 0 00 0未启用偏向锁,00轻量级锁
                //68 f4 a8 1d (01101000 11110100 10101000 00011101)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t1");
​
        thread1.start();
        thread1.join();
    }
Heavyweight lock
    public void heavyLockTest() throws InterruptedException {
        Object obj = new Object();
        ClassLayout objClassLayout = ClassLayout.parseInstance(obj);
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                //第一次 00 表示 轻量级锁
                //d8 f1 c3 1e (11011000 11110001 11000011 00011110)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
​
                //用debug控制t2来竞争
                //第二次打印 变成 10 表示膨胀为重量级锁(t2竞争)  其他62位指向监视器对象
                //fa 21 3e 1a (11111010 00100001 00111110 00011010)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t1");
​
        thread1.start();
​
        Thread thread2 = new Thread(() -> {
            synchronized (obj) {
                //t2竞争 膨胀为 重量级锁 111110 10 10为重量级锁
                //fa 21 3e 1a (11111010 00100001 00111110 00011010)
                //00 00 00 00 (00000000 00000000 00000000 00000000)
                System.out.println(Thread.currentThread().getName() + ":");
                System.out.println(objClassLayout.toPrintable());
            }
        }, "t2");
        thread2.start();
​
        thread1.join();
        thread2.join();
​
        //10 重量级锁 未发生锁降级
        //3a 36 4d 1a (00111010 00110110 01001101 00011010)
        //00 00 00 00 (00000000 00000000 00000000 00000000)
        System.out.println(Thread.currentThread().getName() + ":");
        System.out.println(objClassLayout.toPrintable());
    }

lightweight lock

Lightweight locks are proposed to reduce the overhead caused by traditional heavyweight locks using mutexes (suspension/resumption threads)

When faced with less competition scenarios, the time to acquire the lock is always short, and the overhead of suspending threads in user mode and kernel mode is relatively large. Use lightweight locks to reduce the overhead.

So how is lightweight lock implemented?

Lightweight locks are mainly implemented by lock record, mark word, and CAS. Lock record is stored in the stack frame of the thread to record lock information.

Lock

Check whether the object is in a lock-free state. If the object is in a lock-free state, the mark word will be copied to the displaced mark word in the lock record.

image.png

Then try to use CAS to replace part of the content in the mark word to point to this lock record . If it succeeds, it means that the lock is acquired successfully.

image.png

If the object holds a lock, it will check whether the thread holding the lock is the current thread. In this reentrant case, the record in the lock record is no longer the mark word but null.

In the case of reentrancy, you only need to perform an auto-increment count. When unlocking, a null lock record will be deducted.

image.png

If CAS fails or the thread holding the lock is not the current thread, lock expansion will be triggered.

The key code is as follows:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  //当前对象的mark word
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
​
  //如果当前对象是无锁状态 
  if (mark->is_neutral()) {
    //将mark word复制到lock record
    lock->set_displaced_header(mark);
    //CAS将当前对象的mark word内容替换为指向lock record
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
  } 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");
    //可重入锁 复制null
    lock->set_displaced_header(NULL);
    return;
  }
​
  //有锁并且获取锁的线程不是当前线程 或者 CAS失败 进行膨胀
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
Unlock

Check whether the copied content in the lock record is empty. If it is empty, it means that it is a reentrant lock.

If it is not empty, check whether the mark word points to the lock record. If it points, CAS tries to replace the mark word record pointing to the lock record with the displaced mark word in the lock record (that is, the original mark word).

If the mark word does not point to the lock record or CAS fails, it means there is competition. If other threads fail to lock, the mark word points to the heavyweight lock, which directly expands.

image.png

The key code is as follows:

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here");
​
  //获取复制的mark word
  markOop dhw = lock->displaced_header();
  markOop mark ;
  //如果为空 说明是可重入
  if (dhw == NULL) {
     // Recursive stack-lock.
     // Diagnostics -- Could be: stack-locked, inflating, inflated.
     mark = object->mark() ;
     assert (!mark->is_neutral(), "invariant") ;
     if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
        assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
     }
     if (mark->has_monitor()) {
        ObjectMonitor * m = mark->monitor() ;
        assert(((oop)(m->object()))->mark() == mark, "invariant") ;
        assert(m->is_entered(THREAD), "invariant") ;
     }
     return ;
  }
​
  mark = object->mark() ;
​
  //如果mark word指向lock record
  if (mark == (markOop) lock) {
     assert (dhw->is_neutral(), "invariant") ;
     //尝试CAS将指向lock record的mark word替换为原来的内容
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }
​
  //未指向当前lock record或者CAS失败则膨胀
  ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}

bias lock

Hotspot developers tested that in some scenarios, the same thread always acquires the lock. In this scenario, it is hoped to acquire the lock with less overhead.

When the biased lock is turned on, if it is in a lock-free state, the mark word will be changed to bias a certain thread ID to identify this thread to acquire the lock (the lock is biased towards this thread)

If you are in a biased lock, it may expand to a lightweight lock if it encounters competition. If you want to store consistent hashes, it will also expand to a heavyweight lock.

JDK8 enables bias locks by default. In higher versions of JDK, bias locks are not enabled by default. This may be because the maintenance of bias locks outweighs the benefits, so we will not conduct in-depth research.

Heavyweight lock

object monitor

Use object monitor objects to implement heavyweight locks

The object monitor uses some fields to record information, such as: the object field is used to record the object that is locked, the header field is used to record the mark word of the object that is locked, and the owner field is used to record the thread holding the lock.

The object monitor uses blocking queues to store threads that cannot compete for locks, and uses waiting queues to store threads that call wait to enter the waiting state.

Blocking queue and waiting queue are analogous to AQS and Condition under concurrent packages.

The object monitor uses the cxq stack and the entry list queue to implement the blocking queue. The cxq stack stores competing threads, and the entry list stores relatively stable threads that have failed in competition. Use wait set to implement the waiting queue.

When a thread calls wait, it enters the wait set waiting queue.

When calling notify, it only adds the head node of the waiting queue to cxq and does not wake up the thread to compete.

The real awakening thread is to wake up the head node in the stable queue entry list to compete when the lock is released. At this time, the awakened node may not necessarily be able to grab the lock, because the thread will also spin when entering cxq. Grab locks to achieve unfair locks

If there are no stored threads in the stable entry list, all threads stored in the cxq stack will be stored in the entry list and then awakened. At this time, the later the thread enters the cxq, the sooner it will be awakened (the cxq stack is first in, last out).

In fact, the implementation is similar to AQS. Let’s look at this piece of code:

T1-t6 acquire the same lock and use the t1 thread to block for a while. Subsequent t2-t6 threads are started in sequence. Since the lock cannot be acquired due to rotation, they will be put in sequence.cxq:t2,t3,t4,t5,t6

When t1 releases the lock, since there is no thread in the entry list, the thread in cxq is stored entry list:t6,t5,t4,t3,t2and then wakes up t6.

Since there are no subsequent threads to compete, the final execution order ist1,t6,t5,t4,t3,t2

Object obj = new Object();
new Thread(() -> {
    synchronized (obj) {
        try {
            //输入阻塞
            //阻塞的目的是让  其他线程自旋完未获取到锁,进入cxq栈
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t1").start();
​
//sleep控制线程阻塞的顺序
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t2").start();
​
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t3").start();
​
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t4").start();
​
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t5").start();
​
Thread.sleep(50);
new Thread(() -> {
    synchronized (obj) {
        System.out.println(Thread.currentThread().getName() + " 获取到锁");
    }
}, "t6").start();

After having a general understanding of object monitor, let’s take a look at expansion and spin.

Expansion

There will be four states during expansion, namely

inflated: The mark word lock flag is 10 (2) indicating that it has been inflated and returns to the object monitor directly.

inflation in progress: If other threads are already inflating, wait for a while and then loop through the state to enter the expanded logic.

stack-locked lightweight lock expansion

neutral lock-free expansion

The logic of lightweight locks and lock-free expansion is similar. They both need to create an object monitor object and set some attributes into it (for example: mark word, which object of the lock, which thread holds the lock...), and finally use CAS to Replace mark word to point to object monitor

ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
​
  for (;;) {
      const markOop mark = object->mark() ;
      assert (!mark->has_bias_pattern(), "invariant") ;
​
      // The mark can be in one of the following states:
      // *  Inflated     - just return
      // *  Stack-locked - coerce it to inflated
      // *  INFLATING    - busy wait for conversion to complete
      // *  Neutral      - aggressively inflate the object.
      // *  BIASED       - Illegal.  We should never see this
​
      // CASE: inflated 
      // 已膨胀:查看 mark word 后两位是否为2  是则膨胀完 返回monitor对象
      if (mark->has_monitor()) {
          ObjectMonitor * inf = mark->monitor() ;
          assert (inf->header()->is_neutral(), "invariant");
          assert (inf->object() == object, "invariant") ;
          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid");
          return inf ;
      }
​
      // CASE: inflation in progress - inflating over a stack-lock.
      // 膨胀中: 等待一会 再循环 从膨胀完状态退出
      if (mark == markOopDesc::INFLATING()) {
         TEVENT (Inflate: spin while INFLATING) ;
         ReadStableMark(object) ;
         continue ;
      }
​
      // CASE: stack-locked
      //轻量级锁膨胀
      if (mark->has_locker()) {
          //创建ObjectMonitor
          ObjectMonitor * m = omAlloc (Self) ;
          m->Recycle();
          m->_Responsible  = NULL ;
          m->OwnerIsThread = 0 ;
          m->_recursions   = 0 ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class
          //cas将mark word替换指向ObjectMonitor
          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;         
          //cas 失败 则说明其他线程膨胀成功,删除当前monitor 退出
          if (cmp != mark) {
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }
          markOop dmw = mark->displaced_mark_helper() ;
          assert (dmw->is_neutral(), "invariant") ;
​
          //成功 设置mark word
          m->set_header(dmw) ;
          //设置持有锁的线程
          m->set_owner(mark->locker());
          //设置锁的是哪个对象
          m->set_object(object);
          guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ;
          //修改mark word对象头信息 锁状态 2
          object->release_set_mark(markOopDesc::encode(m));
          if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
          TEVENT(Inflate: overwrite stacklock) ;
          if (TraceMonitorInflation) {
            if (object->is_instance()) {
              ResourceMark rm;
              tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
                (void *) object, (intptr_t) object->mark(),
                object->klass()->external_name());
            }
          }
          return m ;
      }
​
      // CASE: neutral
      //无锁膨胀 与轻量级锁膨胀类似,也是创建monitor对象并注入属性,只是很多属性为空
      assert (mark->is_neutral(), "invariant");
​
      ObjectMonitor * m = omAlloc (Self) ;
      m->Recycle();
      m->set_header(mark);
      m->set_owner(NULL);
      m->set_object(object);
      m->OwnerIsThread = 1 ;
      m->_recursions   = 0 ;
      m->_Responsible  = NULL ;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class
      //cas 更新 mark word 失败循环等待  成功返回
      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
          m->set_object (NULL) ;
          m->set_owner  (NULL) ;
          m->OwnerIsThread = 0 ;
          m->Recycle() ;
          omRelease (Self, m, true) ;
          m = NULL ;
          continue ;
      }
     
      if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
      TEVENT(Inflate: overwrite neutral) ;
      if (TraceMonitorInflation) {
        if (object->is_instance()) {
          ResourceMark rm;
          tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
            (void *) object, (intptr_t) object->mark(),
            object->klass()->external_name());
        }
      }
      return m ;
  }
}
Spin

After expansion, fixed spin and adaptive spin will be performed before final suspension.

Fixed spin defaults to 10+1 times

The adaptive spin starts 5000 times. If the recent competition is less and the lock is obtained, the number of spins will be increased. If the recent competition is high and no lock is obtained, the number of spins will be decreased.

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {
​
    // Dumb, brutal spin.  Good for comparative measurements against adaptive spinning.
    int ctr = Knob_FixedSpin ;
    if (ctr != 0) {
        while (--ctr >= 0) {
            if (TryLock (Self) > 0) return 1 ;
            SpinPause () ;
        }
        return 0 ;
    }
    //先进行固定11自旋次数 获取到锁返回,没获取到空转
    for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {
      if (TryLock(Self) > 0) {
        // Increase _SpinDuration ...
        // Note that we don't clamp SpinDuration precisely at SpinLimit.
        // Raising _SpurDuration to the poverty line is key.
        int x = _SpinDuration ;
        if (x < Knob_SpinLimit) {
           if (x < Knob_Poverty) x = Knob_Poverty ;
           _SpinDuration = x + Knob_BonusB ;
        }
        return 1 ;
      }
      SpinPause () ;
    }
    
    //自适应自旋 一开始5000 如果成功认为此时竞争不大 自旋获取锁成功率高 增加重试次数 如果失败则减少
    //...
}   

Summarize

This article focuses on synchronized, describing in simple terms the implementation of CAS and synchronized at the Java level and C++ level, lock upgrade principles, cases, source code, etc.

synchronized is used in scenarios that require synchronization under concurrency. It can satisfy atomicity, visibility and orderliness. It can be used on ordinary objects and static objects. When it is used on static objects, it is to obtain the lock of its corresponding Class object.

When synchronized acts on a code block, monitorentry and monitorexit bytecode instructions are used to identify locking and unlocking; when synchronized acts on a method, the synchronized keyword is added to the access identifier, and the virtual machine implicitly uses monitorentry and monitorexit

CAS compares and replaces, often implemented with the retry mechanism to implement optimistic locks/spin locks. The advantage is that it can replace thread hangs with less overhead in scenarios with little competition, but it causes ABA problems and cannot predict the number of retries and idles the CPU. expenses and other issues

Lightweight locks are proposed to replace mutexes with smaller overhead in scenarios of alternate execution/less competition; implemented using CAS and lock record

When locking a lightweight lock, if there is no lock, copy the mark word to the lock record, and then CAS replaces the object mark word to point to the lock record. If it fails, it will expand; if the lock is already held, the thread holding the lock will be determined. If it is the current thread, the number of times will be accumulated. If it is not the current thread, it will be expanded.

When the lightweight lock is unlocked, check whether the lock record copied is null. If it is, it means that the lock is reentrant, and the number of times is reduced by one; if not, CAS will replace the copied mark word. If the replacement fails, it means that other threads are competing for mark. word has pointed to the object monitor to point to the release of the heavyweight lock

Biased locking is proposed to replace the overhead of CAS with smaller overhead in scenarios where one thread is often executed. However, higher versions are no longer enabled by default.

Heavyweight locks are implemented by object monitor. In object monitor, cxq and entry list are used to form the blocking queue, and wait set is used to form the waiting queue.

When the wait method is executed, the thread is built as the node joins the wait set; when the notify method is executed, the wait set queue head node is added to cxq, and the entry list queue head node is awakened to compete for the lock only when the lock is released, even if the lock is not grabbed. When a node is added to cxq, it will still spin, so it is not necessarily the head node of the entry list that can grab the lock, so as to achieve unfair locks; when the entry list is empty, add the node in the cxq stack to the entry list queue (Nodes that enter cxq later will be awakened first)

There are four situations when expanding to a heavyweight lock. If the status is expanded, the object monitor object will be returned directly; if the status is expanding, it means that other threads are expanding and will wait. The next loop will enter the expanded logic; if the status For lightweight lock expansion or lock-free expansion, the object monitor object will be created, some important attributes will be set, and CAS will replace the mark word to point to the object monitor.

The heavyweight lock will perform fixed spin and adaptive spin before finally hanging (increase the number of spins if the competition is small recently; reduce the number of spins if the competition is high)

Finally (don’t do it for free, just press three times in a row to beg for help~)

This article is included in the column " From Point to Line, and from Line to Surface" to build a Java concurrent programming knowledge system in simple terms . Interested students can continue to pay attention.

The notes and cases of this article have been included in gitee-StudyJava and github-StudyJava . Interested students can continue to pay attention under stat~

Case address:

Gitee-JavaConcurrentProgramming/src/main/java/B_synchronized

Github-JavaConcurrentProgramming/src/main/java/B_synchronized

If you have any questions, you can discuss them in the comment area. If you think Cai Cai’s writing is good, you can like, follow, and collect it to support it~

Follow Cai Cai and share more useful information, public account: Cai Cai’s back-end private kitchen

Lei Jun: The official version of Xiaomi’s new operating system ThePaper OS has been packaged. A pop-up window on the Gome App lottery page insults its founder. The U.S. government restricts the export of NVIDIA H800 GPU to China. The Xiaomi ThePaper OS interface is exposed. A master used Scratch to rub the RISC-V simulator and it ran successfully. Linux kernel RustDesk remote desktop 1.2.3 released, enhanced Wayland support After unplugging the Logitech USB receiver, the Linux kernel crashed DHH sharp review of "packaging tools": the front end does not need to be built at all (No Build) JetBrains launches Writerside to create technical documentation Tools for Node.js 21 officially released
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/6903207/blog/10112326