Lock optimization means

Synchronized does a lot of lock upgrade control according to practice and theory, so that the cost of lock unlocking is not so heavy. However, THE JVM still thinks that it is not enough, and it also adds many lock optimization methods. This article will introduce these methods, some of which are interesting. It’s even more difficult to understand.

As can be seen from the locking and unlocking process of biased lock, when only one thread repeatedly enters the synchronization block, the performance cost brought by biased lock can be basically ignored. However, when other threads attempt to acquire the lock, they need to wait until the SAFE point of GC to revoke the biased lock to the lockless state or upgrade it to lightweight. Therefore, in the case of frequent multi-thread competition, biased locking can not only improve performance, but also lead to performance degradation, so there are batch rebiased and batch biased lock cancellation mechanisms.

Batch rebias and partial lock batch undo

  • Bulk bias

For each class, a bias lock cancellation counter is maintained. Each time a bias lock cancellation occurs on an object of that class, the counter increases by 1. When this value reaches the bias lock threshold, the JVM considers the bias lock of that class to be problematic. Batch heavy bias will be performed.

  • Batch cancellation

When a batch rebias has been executed, but other threads keep making the rebias lock revocation times of this thread reach the threshold (default 40, I actually tested 36), then the objects of the class corresponding to this lock (including the subsequent creation) will be unlocked.

To see this threshold, add the JVM argument -xx :+PrintFlagsFinal at startup:

intx BiasedLockingBulkRebiasThreshold          = 20 // Default bias lock batch bias threshold
intx BiasedLockingBulkRevokeThreshold          = 40 // Default batch revocation threshold for biased locks
Copy the code

The above is more general, the following through the code to see the phenomenon, after seeing the phenomenon to explain again.

// Batch rebias and partial lock batch undo code demo
public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    List<Object> locks = new ArrayList<>();
    new Thread(() -> {
        for (int i = 0; i < 40; i++) {
            Object lock = new Object();
            synchronized (lock) {
                // bias all 40 lock objects towards thread1 and save them in the LOCKS global list
                locks.add(lock);
            }
        }
        System.out.println("40 Lock objects have been biased to [" + Thread.currentThread().getName() + "], print the Mark word");
        System.out.println(MyClassLayOut.getMarkWord(locks.get(0)) + "\n");
        try {
            TimeUnit.SECONDS.sleep(15);
        } catch (InterruptedException e) {
        }
    }, "thread1").start();
    // Wait long enough for the thread1 operation to complete
    TimeUnit.SECONDS.sleep(3);
    // The creator thread in turn unlocks the 40 thread1-biased objects and upgrades them to lightweight locks to see if they meet the expectations
    new Thread(() -> {
        for (int i = 0; i < 40; i++) {
            Object lock = locks.get(i);
            synchronized (lock) {
                if (i >= 15 && i <= 17) {
                    System.out.println("Printed" + Thread.currentThread().getName() + "] Lock the first (" + (i + 1) + ") mark word\n for lock objects" + MyClassLayOut.getMarkWord(lock) + "\n"); }}}try {
            TimeUnit.SECONDS.sleep(15);
        } catch (InterruptedException e) {
        }
    }, "thread2").start();
    TimeUnit.SECONDS.sleep(8);
    new Thread(() -> {
        for (int i = 0; i < 40; i++) {
            Object lock = locks.get(i);
            synchronized (lock) {
                if (i > 33 && i < 37) {
                    Object obj = new Object();
                    System.out.println("Printed" + Thread.currentThread().getName() + The first (" ") + (i + 1) + Mark word\n of new Object after bias lock on [thread2] is lifted + MyClassLayOut.getMarkWord(obj) + "\n"); }}}},"thread3").start(); } Run result: already will40All lock objects are biased towards [thread1]. Print the Mark word05 30 13 9f (00000101 00110000 00010011 10011111) (-1626132475)
fa 01 00 00 (11111010 00000001 00000000 00000000) (506Print [thread2] again locks the first (16Mark Word b0 f1 for lock objects0f 6a (10110000 11110001 00001111 01101010) (1779429808)
97 00 00 00 (10010111 00000000 00000000 00000000) (151Print [thread2] again locks the first (17Mark Word for lock objects05 09 47 9f (00000101 00001001 01000111 10011111) (-1622734587)
fa 01 00 00 (11111010 00000001 00000000 00000000) (506Print [thread2] again locks the first (18Mark Word for lock objects05 09 47 9f (00000101 00001001 01000111 10011111) (-1622734587)
fa 01 00 00 (11111010 00000001 00000000 00000000) (506Print [thread3] line (35If [thread2] bias lock is removed, create mark word for Object05 01 00 00 (00000101 00000001 00000000 00000000) (261)
00 00 00 00 (00000000 00000000 00000000 00000000) (0Print [thread3] line (36If [thread2] bias lock is removed, create mark word for Object01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0Print [thread3] line (37If [thread2] bias lock is removed, create mark word for Object01 00 00 00 (00000001 00000000 00000000 00000000) (1)
00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Copy the code

Analyze the output:

thread1: In the run method of a thread1 thread, 40 objects are iterated and synchronized, so that all 40 objects are biased towards Thread1 and added to the LOCKS list.

thread2: A thread2 thread’s run method iterates through the locks list, retrieves each Object, and synchronizes it. In lock escalation terms, this causes all 40 objects in the locks list to become lightweight locks. But in fact, it did not. After discovering that the first 16 Object objects were synchronized, it was indeed upgraded to lightweight lock (state 00). However, after the 17th Object Object was synchronized, the Object lock state changed to biased lock state (101). The biased thread was also changed to thread2, obviously not thread1’s biased thread, and even the EPOCH field changed to 01.

Thread3: In a thread3 thread, the locks list is iterated through, fetching each Object and synchronized it. In the first 35 synchronized attempts, an Object Object was created and the initial state of the Object was found to be biased (101, thread ID was 0, epoch was 01). However, from the 36th synchronized attempts, the newly created Object was no longer biased. It is in an unlocked state (001).

The above verification shows that there really are batch rebias and batch revocation of biased lock.

The principle of

For each class, a bias lock cancellation counter is maintained. Each time a bias lock is revoked on an object of that class, the counter increases by 1. When this value reaches the bias threshold (default: 20; I tested it at 17), the JVM considers the bias lock to be problematic. So there will be batch heavy bias.

Each class object has a corresponding EPOCH field in C, as well as in the Mark Word for each object in the biased lock state. The initial value is the epoch value in the class when the object was created. Each time a batch rebias occurs, the EPOCH +1 is added and the stack of all threads in the JVM is simultaneously traversed to find the biased lock that the class is in the locked state (no code block is produced, the lock used by the code block is the object of the class, and the current state is biased lock [101]) and change its EPOCH field to the new value. The next time the lock is acquired, if the epoch of the current object is not the same as the epoch of the class, the undo operation will not be performed even if the current thread is biased to another thread. Instead, change its mark word ThreadID to the current ThreadID directly via cas (if the current thread is the heavily biased thread, otherwise it will be a lightweight lock, as shown in the code below).

After the rebias threshold (20 by default) is reached, assuming that the class’s counter continues to grow, and after it reaches the batch undo threshold (40 by default), the JVM considers the class to be in multithreaded contention, marks it as unbiased, and then creates new objects for that class. It will also be unlocked (001), and subsequent locks will be lightweight locks.

// Demonstrate whether biased locks in synchronized blocks alter the epoch
public static void main(String[] args) throws Exception {
    TimeUnit.SECONDS.sleep(5);
    List<Object> locks = new ArrayList<>();
    new Thread(() -> {
        for (int i = 0; i < 40; i++) {
            Object lock = new Object();
            synchronized (lock) {
                // bias all 40 lock objects towards thread1 and save them in the LOCKS global list
                locks.add(lock);
            }
        }
        System.out.println("40 Lock objects have been biased to [" + Thread.currentThread().getName() + "], print the Mark word");
        System.out.println(MyClassLayOut.getMarkWord(locks.get(0)) + "\n");
        synchronized (locks.get(38)) {
            try {
                System.out.println("Extend the bias time of the 39th bias lock, and the code block sleeps for 40 seconds to ensure that the object is still in the synchronized block when the batch bias occurs \n");
                TimeUnit.SECONDS.sleep(40);
            } catch (InterruptedException e) {
            }
        }
        try {
            TimeUnit.SECONDS.sleep(40);
        } catch (InterruptedException e) {
        }
    }, "thread1").start();
    // Wait long enough for the thread1 operation to complete
    TimeUnit.SECONDS.sleep(3);
    // Print the header of the 39th object to be extended to the synchronized block before and after the batch bias
    new Thread(() -> {
        for (int i = 0; i < 37; i++) {
            Object lock = locks.get(i);
            synchronized (lock) {
                if (i == 14) {
                    System.out.println("Print batch rebias mark Word \n of previous 39th Lock object" + MyClassLayOut.getMarkWord(locks.get(38)) + "\n");
                }
                if (i == 36) {
                    System.out.println("Print the mark word\n of the 39th lock object after batch rebias" + MyClassLayOut.getMarkWord(locks.get(38)) + "\n"); }}}try {
            TimeUnit.SECONDS.sleep(15);
        } catch (InterruptedException e) {
        }
    }, "thread2").start();
    // Wait 45 seconds to make sure that the above 40 seconds sleep is over, otherwise it will be upgraded to heavyweight lock anyway
    TimeUnit.SECONDS.sleep(45);
    new Thread(()->{
        synchronized (locks.get(38)){
            System.out.println("Print mark word\n after the 39th lock object is locked by thread3" + MyClassLayOut.getMarkWord(locks.get(38)) + "\n"); }},"thread3").start(); } Run result: already will40All lock objects are biased towards [thread1]. Print the Mark word05 a0 80 fe (00000101 10100000 10000000 11111110) (-25124859)
ab 01 00 00 (10101011 00000001 00000000 00000000) (427) to extend the first39A bias lock bias time, code block sleep40Seconds guarantee that a batch bias occurs when the object is still in the synchronization block before printing the batch bias39Mark Word for a lock object05 a0 80 fe (00000101 10100000 10000000 11111110) (-25124859)
ab 01 00 00 (10101011 00000001 00000000 00000000) (427) print batch heavy bias after the first39Mark Word for a lock object05 a1 80 fe (00000101 10100001 10000000 11111110) (-25124603)
ab 01 00 00 (10101011 00000001 00000000 00000000) (427Print the first39[thread3] mark word for each lock object70 ee 2f 7f (01110000 11101110 00101111 01111111) (2133847664)
69 00 00 00 (01101001 00000000 00000000 00000000) (105)
Copy the code

The 39th object was biased to thread1 at the beginning, and the time of its biased to the synchronized block where the lock was located was extended. During the operation of Thread2, it was found that the epoch of the object did change from 00 to 01 before and after the batch rebias occurred, and after thread1 released the biased lock, The result of using a thread2 lock is a bias lock, because thread2 is the thread that determines the bias of a batch bias. This proves the second principle mentioned above.

Meaning of existence

Batch heavy bias: One thread creates a large number of objects, performs the initial synchronization, and then another thread operates on those objects as lock objects. This results in a large number of biased lock cancelations, which can be very performance critical, so the JVM introduces the argument that the class’s objects are biased incorrectly. Causes all objects of that class to be rebiased to another thread.

Partial lock batch undo: In obvious multi-threaded interactions, even saying is obvious under the situation of fierce competition, the object of the class to keep the new object can be biased, is clearly not appropriate, because the future is very new object may also have been revoked bias, so simply the object of the class/new object all cancel to license, prevent constantly to affect performance.

conclusion

  1. Batch rebias and batch undo are class-specific optimizations, independent of objects.
  2. Bias lock batch bias once, will not be biased again.
  3. When a class has triggered batch undo of biased locks, the JVM defaults to a serious problem with the current class, depriving new objects of that class of using biased locks.

Spin optimization

Spin optimizations will also be used when heavyweight locks compete. If the current thread spins successfully (even if the lock holding thread has exited the block and released the lock), then the current thread can avoid blocking.

  • Spin takes up CPU time. Single-core CPU spin is wasteful. Multi-core CPU spin takes advantage.

  • After Java6, the spin is adaptive. For example, if the object has just had a successful spin operation, it will think that the probability of successful spin is very high, so it will spin several times more, and the counter will have less spin or no spin, which is more intelligent.

  • After java7, you cannot control whether the spin function is enabled.

Note: The purpose of the spin is to reduce the number of times the thread is suspended and to avoid directly suspending the thread (suspension operation involves system calls and switching between user and kernel states, which is the biggest overhead of heavyweight locks).

Myth: Lightweight locks spin

Lightweight locks do not spin. Lightweight locks can occur in two ways:

  1. The lock object in the biased state (101, the thread ID is not empty, but not in the synchronized block) is determined and upgraded to a lightweight lock.

  2. If the lock object is not locked (001), try cas. Set the Mark word pointer to lock Record. If the CAS fails, the heavyweight lock will be upgraded directly.

Both cases are judged separately, not by spin.

Source code proof: