Java multithreading 2 - synchronized

Keywords: Java Multithreading

1. synchronized concept

1.1 INTRODUCTION

synchronized is different from ReentrantLock mentioned earlier. It is a keyword provided by the Java language. In a way, it is similar to CAS operation (native method).

The synchronized keyword solves the synchronization of accessing resources between multiple threads. The synchronized keyword can ensure that only one thread can execute the modified method or code block at any time.

synchronized enables thread synchronization mainly because it ensures three characteristics of modified statements / values:

  • Atomicity: synchronized ensures that operations within a statement block are atomic
  • Visibility: synchronized to ensure visibility (ensure that "this variable must be synchronized back to main memory before unlock ing" is implemented)
  • Orderliness: synchronized ensures orderliness (ensuring that "only one thread is allowed to lock a variable at the same time")

Heavyweight synchronized

In addition, in earlier versions of Java, synchronized is a heavyweight lock, which is inefficient.

Because the monitor lock is implemented by relying on the Mutex Lock of the underlying operating system, Java threads are mapped to the native threads of the operating system. If you want to suspend or wake up a thread, you need the help of the operating system. When the operating system realizes the switching between threads, it needs to switch from user state to kernel state. The switching between these States takes a relatively long time and the time cost is relatively high.

1.2 implementation principle

As we know, Java is compiled and executed by the JVM virtual machine, which is responsible for managing the execution order and memory resources of Java programs. When a code block / method / class is modified by synchronized, the virtual opportunity detects it and inserts control instructions at a specific location of the compiler.

synchronized is used to * * method or code block * * to ensure that the modified code can only be accessed by one thread at a time.

When synchronized modifies the code block, the JVM uses * * monitorenter and monitorexit * * instructions to achieve synchronization.

The method synchronization is implemented in another way. The details are not described in detail in the JVM specification, but the method synchronization can also be implemented using these two instructions.

The monitorenter instruction is inserted at the beginning of the synchronization code block after compilation, while monitorexit is inserted at the end of the method and at the exception. The JVM must ensure that each monitorenter must have a corresponding monitorexit paired with it.

And monitorenter, monitorexit or ACC_SYNCHRONIZED is * * implemented based on monitor * *. Any object has a monitor associated with it. When a monitor is held, it will be locked. When the thread executes the monitorenter instruction, it will try to obtain the ownership of the monitor corresponding to the object, that is, try to obtain the lock of the object.

When the jvm executes the monitorenter instruction, the current thread attempts to obtain the ownership of the monitor object. If it is not locked or has been held by the current thread, the lock counter is + 1; When the monitorexit instruction is executed, the lock counter is - 1; When the lock counter is 0, the lock is released. If the acquisition of the monitor object fails, the thread will enter a blocking state until another thread releases the lock.

When the synchronized modifier is used, the JVM uses the * * ACC_SYNCHRONIZED * * flag to achieve synchronization

Method level synchronization is implicit, that is, it does not need to be controlled by bytecode instructions. It is implemented in method calls and return operations. The JVM can access ACC from the method_info structure in the method constant pool_ The synchronized access flag distinguishes whether a method is synchronized or not. When a method is called, the calling instruction will check the ACC of the method_ Whether the synchronized access flag is set. If it is set, the execution thread will first hold the monitor (the word pipe is used in the virtual machine specification), then execute the method, and finally release the monitor when the method is completed (whether it is normal or abnormal).

In fact, there is no difference between the synchronized modification method and the underlying principle of the modified variable. It is also competing for the monitor object, but the difference is the timing.

$ javap -verbose SynchronizedDemo.class
Classfile /E:/works_documents/codes/java/test/src/main/java/com/cly/Synchronize/test_10/SynchronizedDemo.class
  Last modified 2020-11-19; size 702 bytes
  MD5 checksum 55a329b6e41aa6de44e1a8a056f17ada
  Compiled from "SynchronizedDemo.java"
public class com.cly.Synchronize.test_10.SynchronizedDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/String
   #3 = Methodref          #2.#25         // java/lang/String."<init>":()V
   #4 = Fieldref           #8.#27         // com/cly/Synchronize/test_10/SynchronizedDemo.a:Ljava/lang/String;
   #5 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #30            // heeee
   #7 = Methodref          #31.#32        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #33            // Synchronize/test_10/SynchronizedDemo
   #9 = Class              #34            // java/lang/Object
  #10 = Utf8               a
  #11 = Utf8               Ljava/lang/String;
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               g
  #17 = Utf8               StackMapTable
  #18 = Class              #33            // Synchronize/test_10/SynchronizedDemo
  #19 = Class              #34            // java/lang/Object
  #20 = Class              #35            // java/lang/Throwable
  #21 = Utf8               main
  #22 = Utf8               ([Ljava/lang/String;)V
  #23 = Utf8               SourceFile
  #24 = Utf8               SynchronizedDemo.java
  #25 = NameAndType        #12:#13        // "<init>":()V
  #26 = Utf8               java/lang/String
  #27 = NameAndType        #10:#11        // a:Ljava/lang/String;
  #28 = Class              #36            // java/lang/System
  #29 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #30 = Utf8               heeee
  #31 = Class              #39            // java/io/PrintStream
  #32 = NameAndType        #40:#41        // println:(Ljava/lang/String;)V
  #33 = Utf8               com/cly/Synchronize/test_10/SynchronizedDemo
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/Throwable
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
  #41 = Utf8               (Ljava/lang/String;)V
{
  public com.cly.Synchronize.test_10.SynchronizedDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/String
         8: dup
         9: invokespecial #3                  // Method java/lang/String."<init>":()V
        12: putfield      #4                  // Field a:Ljava/lang/String;
        15: return
      LineNumberTable:
        line 3: 0
        line 8: 4

  public void g();
    descriptor: ()V
    flags: ACC_PUBLIC			// Modifier blocks are not set in the method access flag
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0						// Load reference type values from local variable 0 into the stack.
         1: getfield      #4 / / get the value String of the object field
         4: dup							// Copy the data of one word at the top of the stack and press the copied data on the stack.
         5: astore_1					// Save the stack top reference type value to local variable 1
         6: monitorenter						// Monitor entry
         7: getstatic     #5                  // Gets the value of a static field #5 
        10: ldc           #6 / / the constant values (int, float, string reference, object reference) in the constant pool are stacked
        12: invokevirtual #7 / / the runtime method binding calls the method.
        15: aload_1							// Load the reference type value from local variable 1 into the stack
        16: monitorexit							// Monitor outlet, release lock
        17: goto          25			// Jump unconditionally to the specified location
        20: astore_2						// Save the stack top reference type value to local variable 2
        21: aload_1							// Load the reference type value from local variable 1 into the stack
        22: monitorexit						// If an exception occurs, release the lock
        23: aload_2							// Load the reference type value from local variable 1 into the stack
        24: athrow							// Throw exception
        25: return
      Exception table:
         from    to  target type
             7    17    20   any
            20    23    20   any
      LineNumberTable:
        line 11: 0
        line 12: 7
        line 13: 15
        line 14: 25
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 20
          locals = [ class com/cly/Synchronize/test_10/SynchronizedDemo, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 18: 0
}

1.4 Monitor object

What about locking objects in memory? Next, let's look at the implementation details of object lock. The status of the object lock is recorded in the Mark word area in the object header. In different lock states, Mark word will store different information, which is also a common design to save memory. When the lock status is a heavyweight lock (lock identification bit = 10), the pointer to the Monitor object will be recorded in Mark word. This Monitor object is also called a process or Monitor lock.

In the HotSpot virtual machine, Monitor is implemented based on the ObjectMonitor class of C + +. Its main members include:

  • _ owner: points to the thread that holds the ObjectMonitor object
  • _ WaitSet: store the thread queue in the wait state, that is, the thread calling the wait() method (blocking queue)
  • _ EntryList: the thread queue (blocking queue) that holds the lock waiting block state
  • _ count: the sum of the number of waiting threads and the number of sleep threads
  • _ cxq: if multiple threads compete for locks, they will be stored in the one-way linked list (ready queue) first
  • _ Recurrences: records the number of reentry times. This counter enables recharging

Why is Synchronized an unfair lock

Unfair locks are mainly reflected in the behavior of obtaining locks. Synchronized does not allocate locks to waiting threads according to the time of applying for locks. After each lock is released, any thread has the opportunity to compete for the lock. This is done a bit in order to improve performance. The disadvantage is that thread starvation may occur.

1.3 synchronized usage scenarios

Generally, the synchronized keyword can modify the scene,

  • Modify instance method: lock the current object instance, and obtain the lock of the current object instance before entering the synchronization code
  • Modify static method: that is, lock the current class, which will act on all object instances of the class. Obtain the lock of the current class before entering the synchronization code.
  • Modifier code block: Specifies the lock object and locks the given object / class.
// Sync code blocks,
synchronized(Account.class){
}

// Synchronization method
public synchronized void deposit(double amt){
}

// Synchronization variable object
synchronized (this){
}

be careful:

  • Modify this to lock the current object
  • Modify A.class to lock the static method of the current class
  • Modification method, divided according to static / non static

1.4 JVM optimization of lock mechanism

1.4.1 lock mechanism in Java SE 1.6

Before JDK 1.6, the synchronized implementation directly called ObjectMonitor's enter and exit. This kind of lock is called "heavyweight lock". At this time, the object has only two lock states: no lock and heavyweight lock. Heavyweight lock involves the switching from user state to kernel state (either execution or blocking), so the efficiency is particularly low.

Therefore, in order to improve efficiency, starting from JDK6, the HotSpot virtual machine development team optimized the locks in Java, such as adding optimization strategies such as adaptive spin, lock elimination, lock coarsening, lightweight lock and bias lock.

1.4.2 deflection lock and lightweight lock

It is found that in most cases, the lock not only does not have multi-threaded competition, but is always obtained by the same thread or a few processes many times. At this time, the thread does not need to wait at all or the waiting time is very short, and there is no need for thread context switching (blocking).

  • Biased lock: when there is no competition, the whole synchronization is eliminated, and CAS operations are not performed. When a thread accesses an object and obtains a lock, the ID of the thread biased by the lock will be stored in the object header. When the thread accesses the object again in the future, it only needs to judge whether there is the thread ID in the Mark Word of the object header. If so, CAS operations are not required

  • Lightweight lock: when the thread competition is more intense, the bias lock will be upgraded to a lightweight lock. The lightweight lock believes that although the competition exists, the degree of competition is very low under ideal conditions. Waiting for the previous thread through spin will release the lock. However, when the spin exceeds a certain number of times, or a thread holds the lock, a thread is spinning, When the third thread access comes (anyway, the competition continues to increase), the lightweight lock will expand into a heavyweight lock.

  • Heavyweight lock: by holding the Monitor, the object that does not hold the Monitor will hang up. The heavyweight lock will block all threads except the thread that owns the lock at this time.

Lightweight lock CAS process

The lightweight CAS updates some bytes in MarkWord to lock record in the thread stack. Lock record: when the JVM detects that the current object is in the unlocked state, it will create a space named lockRecord in the current thread. The user will copy the data in MarkWord. If the update is successful, the lightweight will be successful and marked as lightweight.

1.4.3 lock test

Unlocked:

   <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
    </dependency>

public class Test_1{
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           28 0f cf 16 (00101000 00001111 11001111 00010110) (382668584)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

The lock status of markword is 01, indicating that it is unlocked

Flag bitLock state
001No lock
101Bias lock
x00Lightweight Locking
x10Heavyweight lock
x11GC tag information

A strange phenomenon is that the lock mark of the object header will also change after the ReentrantLock lock.

Locking:

public class Test_1{
    public static void main(String[] args) {
        Object o = new Object();
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           18 f3 9a 02 (00011000 11110011 10011010 00000010) (43709208)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           28 0f d4 16 (00101000 00001111 11010100 00010110) (382996264)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

The lock status of markword is 000, which means it is unlocked. Why?

Lock after 5s sleep:

public class Test_1{
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);

        Object o = new Object();
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 f8 52 02 (00000101 11111000 01010010 00000010) (38991877)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           28 0f d8 16 (00101000 00001111 11011000 00010110) (383258408)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

The lock status of markword is 101, indicating a bias lock

Because JDK1.8 turns on the bias lock after 4s by default, why?

Some people say that this is a compromise. If multiple threads compete for an object, they will immediately enter the lightweight lock. Therefore, in order to avoid the overhead of entering the lightweight lock after directly opening the bias lock, JDK1.8 chooses to open it after 4s.

1.4.4 other optimizations proposed by the JVM

Adaptive spin:

Adaptive means that the spin time is no longer fixed, but is determined by the previous spin time on the same lock and the state of the lock owner:

  • If on the same lock object, the spin wait has just successfully obtained the lock, and the thread holding the lock is running, the virtual machine will think that the spin is likely to succeed again, and then it will allow the spin wait to last for a relatively longer time, such as 100 cycles.
  • On the contrary, if the spin is rarely successfully obtained for a lock, it may reduce the spin time or even omit the spin process when acquiring the lock in the future, so as to avoid wasting processor resources.

Adaptive spin solves the problem of "uncertain lock competition time". It is difficult for the JVM to perceive the exact lock contention time, and leaving it to the user for analysis violates the original intention of the JVM. Adaptive spin assumes that different threads hold the same lock object for the same time, and the degree of competition tends to be stable. Therefore, the time of the next spin can be adjusted according to the time and result of the last spin.

Lock coarsening:

Generally, in order to ensure effective concurrency among multiple threads, each thread is required to hold the lock as short as possible. However, in some cases, a program will consume certain system resources by continuously and frequently requesting, synchronizing and releasing the same lock, because the emphasis on lock, synchronization and release itself will lead to performance loss, In this way, high-frequency lock requests are not conducive to the optimization of system performance, although the time of single synchronization operation may be very short.

for(int i=0;i<size;i++){
    synchronized(lock){	// Each for loop must go through lock application and lock release
    }
}

Lock elimination:

Lock elimination is a lock optimization method that occurs at the compiler level.
Sometimes the code we write does not need to be locked at all, but performs the locking operation.

@Override
public synchronized int append() {
    int a = 20;	// The method is internally operated on private variables, which is thread safe
    return a;
}

2. Control method

2.1 difference between sleep () and wait() methods

  • The main difference between the two is that the sleep() method does not release the lock, but only releases the CPU resources, while the wait() method releases the lock and CPU resources
  • Both can pause thread execution.
  • wait() is usually used for inter thread interaction / communication, and sleep() is usually used to pause execution.
  • After the wait() method is called, the thread will not wake up automatically. Other threads need to call the notify() or notifyAll() method on the same object. After the sleep() method is executed, the thread will wake up automatically. Alternatively, you can use wait(long timeout) to automatically wake up the thread after timeout.

synchronized enables alternate printing

class print implements Runnable{
    private static int count = 1;
    private final static Object lock = new Object();
    
    @Override
    public void run() {
        while(count <= 10){
            synchronized (lock){
                System.out.println(Thread.currentThread().getName() + ":" + count++);
                lock.notify();
                if(count <= 10){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

public class Test_2 {
    public static void main(String[] args) {
        new Thread(new print(),"Thread 1").start();
        new Thread(new print(),"Thread 2").start();
    }
}

Posted by barrylee on Wed, 29 Sep 2021 12:15:00 -0700