Chapter 11 Detailed explanation of synchronized lock upgrade

Upgrade process

Why should we introduce the process of lock upgrade?

image-20230620232505153

  • Java threads are mapped to the native threads of the operating system. If you want to block or wake up a thread, you need the intervention of the operating system (our Monitor object corresponds to a blocking queue _EntryList), and you need to switch between the user state and the core state , this switch will consume a lot of system resources, because both user mode and kernel mode have their own dedicated memory space, dedicated registers, etc., switching from user mode to kernel mode needs to pass many variables and parameters to the kernel, and the kernel also needs to protect Keep some register values, variables, etc. when switching to user mode, so that you can switch back to user mode and continue working after the kernel mode call is completed.
    • The trap instruction is executed in the user mode. After executing the trap instruction, an internal interrupt will be triggered immediately, so that the CPU enters the core state and switches from the user mode to the kernel mode through the trap instruction.
  • In the early versions of Java, synchronized is a heavyweight lock, which is inefficient, because the monitor lock (monitor) is implemented relying on the underlying operating system. Suspending threads and restoring threads need to be transferred to the kernel state to complete, blocking Or waking up a Java thread requires the operating system to switch the CPU state to complete. This state switching takes processor time. If the content of the synchronization code block is too simple, the switching time may be longer than the execution time of the user code", time The cost is relatively high, which is why the early synchronized was inefficient. After Java 6, in order to reduce the performance consumption caused by acquiring and releasing locks, lightweight locks and biased locks were introduced.

Multi-threaded access

  • There is only one thread to access, and there is only One
  • There are multiple threads (2 threads A, B to access alternately)
    • They do not compete for locks at the same time. For example, after A has accessed, A releases the lock, and then B comes to access, which is staggered.
  • Competition is fierce, more threads come to access
    • The competition is fierce. For example, A occupies the lock, and B also wants to occupy the lock, causing competition

specific process

  • The lock used by synchronized is stored in the Mark Word in the Java object header. The lock upgrade function mainly depends on the lock flag and release bias lock flag in MarkWord.

image-20230620215312621

  • Biased lock: MarkWord stores a biased thread ID ;
  • Lightweight lock: MarkWord stores a pointer to the Lock Record in the thread stack ;
  • Weight lock: MarkWord stores pointers to monitor objects in the heap ;

img

  • Put the lock information into the object header
  • It starts with optimistic locking, and if lock conflicts occur frequently, it is converted to pessimistic locking.
  • At the beginning, it is a lightweight lock implementation. If the lock is held for a long time, it will be converted into a heavyweight lock.
  • The spin lock strategy that is most likely to be used when implementing lightweight locks
  • is an unfair lock
  • is a reentrant lock
  • not read-write lock

The heavyweight lock process of synchronized after introducing the object header

insert image description here

lightweight lock

  • Usage scenarios of lightweight locks: If an object needs to be locked by multiple threads, but the locking time is staggered (that is, there is no competition), then lightweight locks can be used for optimization.
    • Lightweight lock : multi-thread competition, but at most one thread competes at any time , that is, there is no fierce competition for locks, and there is no thread blocking.

main effect

  • There are threads competing for the lock, but the conflict time to acquire the lock is extremely short.
  • The essence is self-selected lock CAS

Suppose there are two methods to synchronize blocks and use the same object to lock

public class SynchronizedDemo1 {
    
    
    static final Object obj = new Object();
    public static void method1() {
    
    
        synchronized( obj ) {
    
    
            // 同步块 A
            method2();
        }
    }
    public static void method2() {
    
    
        synchronized( obj ) {
    
    
            // 同步块 B
        }
    }
}

image-20230620235307246

  • Create a Lock Record object. The stack frame of each thread will contain a lock record structure, which can store the Mark Word of the lock object internally.

How to use CAS to implement lightweight locks

CAS lock successful

image-20230620235328482

  • Why does our lock record address have two 0s after it?
    • We can see in the picture above that the status of our lightweight lock is 00
    • Our CAS (Comparison and Interaction), it is this status bit that makes the judgment
      • v AB corresponds to v being the value in Object in memory. The lock status bit of the object A is 01 and B is 00.
      • When the status bit in our Object is 01, it is either lock-free or biased lock, and lightweight lock can be added.
  • If the cas replacement is successful, the lock record address and status 00 are stored in the object header, indicating that the object is locked by this thread.

CAS lock failed

  • If other threads already hold the lightweight lock of the Object, it indicates that there is competition and the lock expansion process is entered.
  • If you perform synchronized lock reentry yourself, add another Lock Record as the reentry count.

image-20230620235347387

  • Why does CAS fail? Because our expected value is 01, but the lock flag of the Object object in memory is 00, so the CAS operation fails.
  • When exiting the synchronized code block (when unlocking), if there is a lock record with a value of null, it means there is reentrancy. At this time, the lock record is reset, which means the reentrancy count is reduced by one.

The processing of replacement failure is to perform a spin operation, that is, to perform a CAS operation in a loop, so it is also called a spin lock. When our number of spins reaches 10 times, we enter the corresponding lock expansion, that is, it becomes a weight. class lock

CAS to unlock

When exiting the synchronized code block (when unlocking), the value of the lock record is not null. At this time, CAS is used to restore the value of Mark Word to the object header.

  • If successful, the unlocking is successful.
  • Failure indicates that the lightweight lock has undergone lock expansion or has been upgraded to a heavyweight lock, and the heavyweight lock unlocking process is entered.

Summarize

Acquisition of lightweight locks

  • Lightweight locks are intended to improve performance when threads execute synchronized blocks nearly alternately.

  • The main purpose: to reduce the performance consumption caused by heavyweight locks using operating system mutexes through CAS without multi-thread competition. To put it bluntly, spin first, and then upgrade to blocking if it doesn't work.

  • Upgrade timing: When the bias lock function is turned off or multi-threads compete for the bias lock, the bias lock will be upgraded to a lightweight lock.

Locking of lightweight locks

  • The JVM will create a space for storing lock records in the current thread's stack frame for each thread, which is officially called Displaced Mark Word. If a thread acquires a lock and finds that it is a lightweight lock, it will copy the Mark Word of the lock to its own Displaced Mark Word. The thread then tries to use CAS to replace the lock's MarkWord with a pointer to the lock record. If it succeeds, the current thread acquires the lock. If it fails, it means that the Mark Word has been replaced by the lock record of other threads, indicating that it is competing with other threads for the lock, and the current thread tries to use spin to acquire the lock.

  • Spin CAS: Keep trying to acquire the lock. If you can't upgrade, don't push it up. Try not to block it.

Release of lightweight locks

  • When the lock is released, the current thread will use the CAS operation to copy the contents of the Displaced Mark Word back to the locked Mark Word. If there is no race, the copy operation will succeed. If other threads have upgraded the lightweight lock to a heavyweight lock due to multiple spins, the CAS operation will fail, and the lock will be released and the blocked thread will be woken up.

When does it become a heavyweight lock?

Spin reaches a certain number and degree
before java6

  • Enabled by default, when the number of spins is ten
  • Or the number of spins exceeds half the number of cores

After java6

  • If the thread spins successfully, the maximum number of spins for the next time will increase, because the JVM believes that since the last time it succeeded, there is a high probability that it will succeed this time.

on the contrary

  • If spin is rarely successful, the number of spins will be reduced or even stopped next time to avoid idling of the CPU.

    • In short, self-adaptation means that the number of self-selections is not fixed, but is determined according to: the time of one spin on the same lock and the state of the thread that owns the lock.

The difference between lightweight locks and biased locks

  • When the fight for a lightweight lock fails, spin attempts to seize the lock.
  • A lightweight lock needs to release the lock every time it exits a synchronized block, while a biased lock only releases the lock when competition occurs.

lock expansion

If the CAS operation fails during the process of trying to add a lightweight lock, then one situation is that other threads have added a lightweight lock (with contention) to this object. At this time, lock expansion is required, and the lightweight The volume lock becomes a heavyweight lock.

static Object obj = new Object();
public static void method1() {
    
    
	synchronized( obj ) {
    
    
		// 同步块
	}
}

image-20230621125752248

At this time, Thread-1 fails to add a lightweight lock and enters the lock expansion process.

  • That is, apply for a Monitor lock for the Object object and let the Object point to the heavyweight lock address.
    • 10 indicates that the lock status is a heavyweight lock
    • Monitor does not appear when it becomes a heavyweight lock, but exists when the object is generated.
  • Then enter the Monitor's EntryList BLOCKED yourself

When Thread-0 exits the sync block and is unlocked, CAS is used to restore the value of Mark Word to the object header, which fails. At this time, the heavyweight unlocking process will be entered, that is, find the Monitor object according to the Monitor address, set the Owner to null, and wake up the BLOCKED thread in the EntryList.

image-20230620235754768

  • When it becomes a heavyweight lock, the corresponding hashcode value is placed in the Monitor object.

spin optimization

When heavyweight locks compete, spin can also be used for optimization. If the current thread spins successfully (that is, the lock-holding thread has exited the synchronization block and released the lock), the current thread can avoid blocking.
Spin retry success

thread 1 (on core 1) Object Mark Thread 2 (on core 2)
- 10 (weight lock) -
Access the synchronization block and obtain the monitor 10 (weight lock) weight lock pointer -
success (lock) 10 (weight lock) weight lock pointer -
execute synchronized block 10 (weight lock) weight lock pointer -
execute synchronized block 10 (weight lock) weight lock pointer Access the synchronization block and obtain the monitor
execute synchronized block 10 (weight lock) weight lock pointer spin retry
Finished 10 (weight lock) weight lock pointer spin retry
success (unlock) 01 (no lock) spin retry
- 10 (weight lock) weight lock pointer success (lock)
- 10 (weight lock) weight lock pointer execute synchronized block
-

Spin retry failure situation

Thread 1 (on core 1) Object Mark Thread 2 (on core 2)
- 10 (weight lock) -
Access the synchronization block and obtain the monitor 10 (weight lock) weight lock pointer -
success (lock) 10 (weight lock) weight lock pointer -
execute synchronized block 10 (weight lock) weight lock pointer -
execute synchronized block 10 (weight lock) weight lock pointer Access the synchronization block and obtain the monitor
execute synchronized block 10 (weight lock) weight lock pointer spin retry
execute synchronized block 10 (weight lock) weight lock pointer spin retry
execute synchronized block 10 (weight lock) weight lock pointer spin retry
execute synchronized block 10 (weight lock) weight lock pointer block
-
  • Spin will take up CPU time, single-core CPU spin is a waste, multi-core CPU spin can take advantage.
  • After Java 6, the spin lock is adaptive. For example, if the object's spin operation has just been successful, then if you think the probability of the spin success will be high, you will spin more times; otherwise, you will spin less or even Not spinning, in short, more intelligent.
  • After Java 7, it is not possible to control whether to turn on the spin function

bias lock

When thread A competes for the lock for the first time, by modifying the bias ID and bias mode in Mark Word. A thread holding a biased lock will never need to synchronize if there are no other threads competing.

main effect

When a piece of synchronized code has been accessed multiple times by the same thread, since only one thread accesses it, the thread will automatically acquire the lock on subsequent accesses.

  • When there is no competition for lightweight locks (just this thread), CAS operations still need to be performed every time reentry occurs.
  • Java 6 introduces biased locks for further optimization: only the first time you use CAS to set the thread ID to the Mark Word header of the object, and later find that the thread ID is your own, it means there is no competition, and you don't need to re-CAS. As long as no competition occurs in the future, this object will be owned by the thread.
public class SynchronizedDemo2 {
    
    
    static final Object obj = new Object();

    public static void main(String[] args) {
    
    
        m1();
    }
    public static void m1() {
    
    
        synchronized( obj ) {
    
    
            // 同步块 A
            m2();
        }
    }
    public static void m2() {
    
    
        synchronized( obj ) {
    
    
            // 同步块 B
            m3();
        }
    }
    
    public static void m3() {
    
    
        synchronized( obj ) {
    
    
        }
    }
}

image-20230621132950459

  • If it is a lightweight lock, even if it is the same thread, the corresponding Lock Record will be generated, and the CAS operation will be performed with the Mard Word of the corresponding object, but the CAS will not be successful.
    • The atomicity of CAS is actually implemented by the CPU. In fact, there is still an exclusive lock at this point , but the exclusive time here is much shorter than using a heavyweight lock (Monitor->Mutex Lock).
  • Only when you use CAS to set the thread ID to the Mark Word header of the object for the first time, and then find that the thread ID is your own, it means that there is no competition, and you don't need to re-CAS. As long as no competition occurs in the future, this object will be owned by the thread.

biased state

|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|

When an object is created:

  • If bias locking is enabled (enabled by default), then after the object is created, the markword value is 0x05, that is, the last three bits are 101, and its thread, epoch, and age are all 0.
  • The biased lock is delayed by default and will not take effect immediately when the program starts. If you want to avoid the delay, you can add the VM parameter -XX:BiasedLockingStartupDelay=0 to disable the delay
  • If the bias lock is not enabled, after the object is created, the mark value is 0x01, that is, the last 3 digits are 001. At this time, its hashcode and age are both 0, and the value will be assigned when the hashcode is used for the first time.

test

Test latency characteristics and biased locking

 // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
    public static void main(String[] args) throws IOException {
    
    
        Dog d = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(d);
        new Thread(() -> {
    
    
            log.debug("synchronized 前");
            System.out.println(classLayout.toPrintableSimple(true));
            synchronized (d) {
    
    
                log.debug("synchronized 中");
                System.out.println(classLayout.toPrintableSimple(true));
            }
            log.debug("synchronized 后");
            System.out.println(classLayout.toPrintableSimple(true));
        }, "t1").start();
    }
11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
  • Lock status 101 indicates that the initial lock is biased. Proved that the bias lock is enabled by default
  • After the object in the biased lock is unlocked, the thread id is still stored in the object header.

test disabled

  • When the above test code is running, add the VM parameter -XX:-UseBiasedLocking to disable biased locking.
11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  • Lock status 001 indicates that there is no lock initially, which proves that the bias lock is prohibited from being opened.
  • When entering the synchronized code, the lock status is 00, indicating that it directly becomes a lightweight lock.
  • After unlocking, it directly becomes the unlocked state.

Test hashCode

  • The normal state object does not have hashCode at the beginning, and it is generated only after the first call.

Cancel bias lock

  • When other threads gradually come to compete for the lock, biased locks can no longer be used and must be upgraded to lightweight locks.
    • Biased locks use a mechanism that waits until competition occurs before releasing the lock. Only when other threads compete for the lock, the original thread holding the biased lock will be revoked.
  • The competing thread attempts to CAS to update the object header and fails, and will wait until the global safe point (no code will be executed at this time) to cancel the bias lock.
    • Undoing requires waiting for a global safe point (no bytecode is executing at this point in time) and checking whether the thread holding the biased lock is still executing:

Java 15 gradually abandons bias locks

But the performance improvements seen in the past are no longer so obvious now. Applications that benefit from biased locking tend to be programs that use the early Java collection APIs (JDK 1.1), which (Hasttable and Vector) are synchronized on each access. JDK 1.2 introduced asynchronous collections (HashMap and ArrayList) for single-threaded scenarios, and JDK 1.5 introduced a higher-performance concurrent data structure for multi-threaded scenarios. This means that applications that benefit from biased locking due to unnecessary synchronization may see large performance gains if the code is updated to use the newer classes. Additionally, the performance of applications built around thread pool queues and worker threads often becomes better with biased locking disabled.

Biased locking introduces a lot to the synchronization system 复杂的代码and has an impact on other components of HotSpot. This complexity has become a barrier to understanding the code and hindering the refactoring of synchronization systems. Therefore, we want to disable, deprecate, and eventually remove biased locks.

Undo - call object hashCode

  • The hashCode of the object is called, but the thread id is stored in the biased lock object MarkWord. If hashCode is called, the biased lock will be revoked.
    • The lightweight lock will record the hashCode in the lock record.
    • Heavyweight locks will record hashCode in Monitor
  • Use biased locking after calling hashCode, remember to remove -XX:-UseBiasedLocking
   public static void main(String[] args) throws IOException {
    
    
        Dog d = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(d);
        log.debug(d.hashcode());
        new Thread(() -> {
    
    
            log.debug("synchronized 前");
            System.out.println(classLayout.toPrintableSimple(true));
            synchronized (d) {
    
    
                log.debug("synchronized 中");
                System.out.println(classLayout.toPrintableSimple(true));
            }
            log.debug("synchronized 后");
            System.out.println(classLayout.toPrintableSimple(true));
        }, "t1").start();
    }
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
  • We found that our lock status is from 001 (no lock) -> 00 (lightweight lock)
  • Indicates that the hashCode method was called and the bias lock was cancelled.

Undo - object used by other threads

  • When other threads use the biased lock object, the biased lock will be upgraded to a lightweight lock.
    private static void test2() throws InterruptedException {
    
    
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
    
    
            synchronized (d) {
    
    
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
             }
            synchronized (TestBiased.class) {
    
    
                TestBiased.class.notify();
             }
			// 如果不用 wait/notify 使用 join 必须打开下面的注释
			// 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
			/*try {
				System.in.read();
			} catch (IOException e) {
				e.printStackTrace();
			}*/
        }, "t1");
        t1.start();
        Thread t2 = new Thread(() -> {
    
    
            synchronized (TestBiased.class) {
    
    
                try {
    
    
                    TestBiased.class.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
    
    
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }, "t2");
        t2.start();
    }
[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  • We use wait/notify here to control thread synchronization (the Lock here is TestBiased.class, not the Dog object), in order to allow the t1 and t2 threads to stagger the locks instead of fighting for the locks at the same time, because fighting for the locks at the same time will lead to heavyweight lock
  • t1 gets the Dog object first, so the lock status is 101, so it is a biased lock, and the id of the t1 thread is stored
  • When t2 goes to acquire the lock, it prefers to upgrade the lock and become a lightweight lock 101->00
  • When t2 is unlocked, it becomes unlocked 00->001

Undo - call wait/notify

 public static void main(String[] args) throws InterruptedException {
    
    
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
    
    
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
    
    
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
                try {
    
    
                    d.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }, "t1");
        t1.start();
        new Thread(() -> {
    
    
            try {
    
    
                Thread.sleep(6000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            synchronized (d) {
    
    
                log.debug("notify");
                d.notify();
            }
        }, "t2").start();
    }
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101
[t2] - notify
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
  • We use wait/notify here. The object used here is the Dog object
  • We use wait/notify here from 101 (lightweight lock) -> 10 (heavyweight lock)
  • Because the bottom layer of our wait and notify needs Monitor to implement

batch rebiasing

If the object is accessed by multiple threads, but there is no competition, the object that is biased towards thread T1 will still have the opportunity to be biased back to T2. Re-biasing will reset the Thread ID of the object.

  • When the biased lock threshold is canceled more than 20 times, the JVM will feel that I have made the wrong bias, so it will redirect to the locking thread when locking these objects.
   private static void test3() throws InterruptedException {
        Vector<Dog> list = new Vector<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }
            synchronized (list) {
                list.notify();
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (list) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("===============> ");
            for (int i = 0; i < 30; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }, "t2");
        t2.start();
    }
  • Wait/notify is performed on the list object to prevent t1 and t2 threads from generating competing locks at the same time.
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
....
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============>
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
....
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  • We found that when only one thread t1 is locked, all the locks are biased (101)
  • When the t1 thread finishes executing, when the t2 thread locks the corresponding objects, we see that the first 19 objects, because of the bias lock, change from the bias lock to a lightweight lock 101->00 (lock upgrade)
  • But from the 20th time, we did not change the bias lock into a lightweight lock, but changed the bias from bias t1 to bias t2.

batch undo

When the biased lock cancellation threshold exceeds 40 times, the JVM will feel that it is indeed biased in the wrong direction and should not be biased at all. Then all objects of the entire class will become non-biasable, and newly created objects will also be non-biasable.

	 static Thread t1,t2,t3;
    private static void test4() throws InterruptedException {
        Vector<Dog> list = new Vector<>();
        int loopNumber = 39;
        t1 = new Thread(() -> {
            for (int i = 0; i < loopNumber; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }
            LockSupport.unpark(t2);
        }, "t1");
        t1.start();
        t2 = new Thread(() -> {
            LockSupport.park();
            log.debug("===============> ");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            LockSupport.unpark(t3);
        }, "t2");
        t2.start();
    	 t3 = new Thread(() -> {
         LockSupport.park();
         log.debug("===============> ");
         for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
         }
    	 }, "t3");
		t3.start();
		t3.join();
		log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}

Summary - Frequent test points in interviews

After the lock upgrade occurs, where does the hashcode go?

After the lock is upgraded to a lightweight or heavyweight lock, Mark Word saves the lock record pointer and heavyweight lock pointer in the thread stack frame respectively. There is no place to save the hash code and GC age. So this information Where was it moved to?

  • In a lock-free state, Mark Word can store the identity hash code value of the object. When the object's hashCode() method is called for the first time, the JVM will generate the corresponding identity hash code value and store the value in Mark Word.

  • For biased locks, when a thread acquires a biased lock, the location of the identity hash code will be overwritten with the Thread ID and epoch value. If an object's hashCode() method has been called once, the object cannot be set with a bias lock. Because if possible, the identity hash code in Mark Word will definitely be overwritten by the biased thread ID, which will cause inconsistent results from calling the hashCode() method twice on the same object.

    • Therefore, when an object is in a biased lock state and receives a request to calculate a consistent hash code, its biased state will be revoked immediately, and the lock will expand into a heavyweight lock.
  • When upgrading to a lightweight lock, the JVM will create a lock record (Lock Record) space in the stack frame of the current thread to store a Mark Word copy of the lock object. This copy can contain the identity hash code, so lightweight The lock can coexist with the identity hash code. The hash code and GC age are naturally stored here. After the lock is released, this information will be written back to the object header.

  • After upgrading to a heavyweight lock, the heavyweight lock pointer saved by Mark Word has a field in the ObjectMonitor class that represents the heavyweight lock to record the Mark Word in the non-locked state. After the lock is released, the information will also be written back to the object header.

lock elimination

  • This lock object is not shared and spread to other threads for use.
  • In the extreme, there is no underlying machine code for adding this lock object, eliminating the use of locks.
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
    
    
    static int x = 0;
    @Benchmark
    public void a() throws Exception {
    
    
       x++;
     }
    @Benchmark
    public void b() throws Exception {
    
    
       Object o = new Object();
          synchronized (o) {
    
    
          x++;
       }
    }
}

java -jar benchmarks.jar

Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op
c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op

java -XX:-EliminateLocks -jar benchmarks.jar - disable lock elimination

Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op
c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op

lock roughening

  • Locking the same object multiple times will cause multiple thread re-entries. You can use lock coarsening to optimize.

If the first position in the method is connected, and the adjacent ones are the same lock object, then the JIT compiler will combine these synchronized blocks into one large block, bold and widen the scope, and apply for one time to avoid multiple lock objects. Apply and release locks multiple times, improving performance

public class SynchronizedDemo3 {
    
    
    static Object objectLock = new Object();
    public static void main(String[] args) {
    
    
        new Thread(()->{
    
    
            synchronized (objectLock){
    
    
                System.out.println("11111111");
            }
            synchronized (objectLock){
    
    
                System.out.println("22222222");
            }
            synchronized (objectLock){
    
    
                System.out.println("33333333");
            }
            synchronized (objectLock){
    
    
                System.out.println("44444444");
            }
            synchronized (objectLock){
    
    
                System.out.println("11111111");
                System.out.println("22222222");
                System.out.println("33333333");
                System.out.println("44444444");
            }
        },"t1").start();

    }
}

Guess you like

Origin blog.csdn.net/qq_50985215/article/details/131329742