JAVA concurrent programming – synchronized keyword

Introduction

In order to solve the problems of atomicity, visibility and order in concurrent programming, the Java language provides a series of keywords related to concurrent processing, such as synchronized, volatile , final, concurren package, etc.

The synchronized keyword can be used as one of the solutions when the three characteristics of atomicity, visibility and order are required, and it seems to be “universal”. Indeed, most concurrency control operations can be done using synchronized.

Hemingway said in his “Death in the Afternoon”: “The majestic movement of the iceberg is because only one-eighth of it is on the water.” For programmers, synchronized is just a key It’s just words, and it’s easy to use. The reason why we can deal with multi-threading issues without thinking too much is because this keyword helps us shield a lot of details.

Then, this article revolves around synchronized, mainly introducing the usage of synchronized, the principle of synchronized, and how synchronized works Provides atomicity, visibility, and order guarantees, etc.

usage of synchronized

synchronized is a keyword for concurrency control provided by Java. There are two main uses, which are synchronized methods and synchronized code blocks. In other words, synchronized can modify both methods and code blocks.

public class SynchronizedDemo {
     //synchronous method
    public synchronized void doSth(){
        System.out.println("Hello World");
    }

    //synchronous code block
    public void doSth1(){
        synchronized (SynchronizedDemo. class) {
            System.out.println("Hello World");
        }
    }
}

Code blocks and methods modified by synchronized can only be accessed by a single thread at the same time.

Implementation principle of synchronized

synchronized is a very important keyword in Java to solve data synchronization access under concurrent conditions. When we want to ensure that a shared resource is only accessed by one thread at a time, we can use the synchronized keyword to lock the class or object in the code.

public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc #3 // String Hello World
         5: invokevirtual #4 // Method java/io/PrintStream. println:(Ljava/lang/String;)V
         8: return

  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc #5 // class com/hollis/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc #3 // String Hello World
        10: invokevirtual #4 // Method java/io/PrintStream. println:(Ljava/lang/String;)V
        13: aload_1
        14: monitor exit
        15: goto 23
        18: astore_2
        19: aload_1
        20: monitor exit
        21: aload_2
        22: athrow
        23: return

It can be seen from the decompiled code that for the synchronization method, the JVM uses the ACC_SYNCHRONIZED tag to achieve synchronization. For synchronous code blocks. The JVM uses two instructions monitorenter and monitorexit to achieve synchronization.

In The Java? Virtual Machine Specification, there is an introduction to the implementation principles of synchronization methods and synchronization code blocks. I translate it into Chinese as follows:

Synchronization at the method level is implicit. There will be a ACC_SYNCHRONIZED flag in the constant pool of the synchronization method. When a thread wants to access a method, it will check whether there is ACC_SYNCHRONIZED, if it is set, you need to obtain the monitor lock first, then start to execute the method, and then release the monitor lock after the method is executed . At this time, if other threads request to execute the method, they will be blocked because they cannot obtain the monitor lock. It is worth noting that if an exception occurs during method execution and the exception is not handled inside the method, the monitor lock will be automatically released before the exception is thrown outside the method.

The synchronous code block is realized by two instructions monitorenter and monitorexit. The execution of the monitorenter command can be understood as locking, and the execution of monitorexit can be understood as releasing the lock. Each object maintains a counter that records the number of times it has been locked. The counter of the unlocked object is 0. When a thread acquires the lock (execute monitorenter), the counter increments to 1. When the same thread acquires the lock of the object again, The counter is incremented again. When the same thread releases the lock (executes the monitorexit instruction), the counter is decremented again. when the counter is 0. The lock will be released and other threads can acquire the lock.

Whether it is ACC_SYNCHRONIZED or monitorenter, monitorexit are all implemented based on Monitor. In the Java virtual machine (HotSpot), Monitor is based on C ++ Realized by ObjectMonitor.

Several methods are provided in the ObjectMonitor class, such as enter, exit, wait, notify, notifyAll etc. When synchronized is locked, the enter method of objectMonitor will be called, and when it is unlocked, the exit method will be called.

synchronized and atomicity

Atomicity means that an operation is uninterruptible, and it must be executed completely, or it will not be executed at all.

Thread is the basic unit of CPU scheduling. The CPU has the concept of time slices, and will perform thread scheduling according to different scheduling algorithms. When a thread starts executing after obtaining the time slice, after the time slice is exhausted, it will lose the right to use the CPU. Therefore, in a multi-threaded scenario, since time slices are rotated between threads, atomicity problems will occur.

In Java, in order to ensure atomicity, two advanced bytecode instructions monitorenter and monitorexit are provided. As mentioned earlier, the corresponding keyword in Java for these two bytecode instructions is synchronized.

Through the monitorenter and monitorexit instructions, you can ensure that the code modified by synchronized can only be accessed by one thread at a time. Before the lock is released, Cannot be accessed by other threads. Therefore, synchronized can be used in Java to ensure that operations within methods and code blocks are atomic.

When thread 1 executes the monitorenter command, it will lock the Monitor. After locking, other threads cannot obtain the lock unless thread 1 actively unlocks it. Even during execution, for some reason, such as the CPU time slice is exhausted, thread 1 gives up the CPU, but he does not unlock it. And because the lock of synchronized is reentrant, the next time slice can only be obtained by himself, and the code will continue to be executed. until all codes are executed. This guarantees atomicity.

synchronized and visibility

Visibility means that when multiple threads access the same variable, and one thread modifies the value of the variable, other threads can immediately see the modified value.

The Java memory model stipulates that all variables are stored in the main memory, and each thread has its own working memory. The working memory of the thread stores the copy of the main memory copy of the variables used in the thread. All operations must be performed in working memory, and cannot directly read and write main memory. Different threads cannot directly access variables in each other’s working memory, and the transfer of variables between threads requires data synchronization between their own working memory and main memory. Therefore, it may happen that thread 1 changes the value of a variable, but thread 2 is not visible.

As we mentioned earlier, the code modified by synchronized will be locked when it starts to execute, and will be unlocked after execution is completed. In order to ensure visibility, there is a rule like this: Before unlocking a variable, the variable must be synchronized back to the main memory. After unlocking in this way, subsequent threads can access the modified value.

Therefore, the value of the object locked by the synchronized keyword is visible.

synchronized and orderly

Orderliness means that the sequence of program execution is executed in the sequence of codes.

In addition to the introduction of time slices, due to processor optimization and instruction rearrangement, the CPU may also perform out-of-order execution of input codes. For example, load->add->save may be optimized into load->save->add. This is where there may be an orderly problem.

It should be noted here that synchronized cannot prohibit instruction rearrangement and processor optimization. In other words, synchronized cannot avoid the problems mentioned above.

So, why do you say that synchronized also provides order guarantee?

This is to expand the concept of orderliness. The natural order in Java programs can be summed up in one sentence: If observed in this thread, all operations are naturally ordered. If one thread observes another, all operations are unordered.

The above sentence is also the original sentence in “In-depth Understanding of Java Virtual Machine”, but how to understand it? Zhou Zhiming did not explain in detail. Let me briefly expand here, which is actually related to as-if-serial semantics.

as-if-serial semantic means: no matter how reordering (compiler and processor to improve parallelism), the execution result of single-threaded program cannot be changed. No matter how the compiler and processor optimize, they must obey the as-if-serial semantics.

The as-if-serial semantics is not expanded here in detail. Simply put, as-if-serial semantics guarantees that there are certain restrictions on instruction rearrangement in a single thread. Yes, and as long as the compiler and processor comply with this semantics, then the single-threaded program can be considered to be executed sequentially. Of course, there are actually rearrangements, but we don’t need to care about the interference of such rearrangements.

Therefore, due to the code modified by synchronized, it can only be accessed by the same thread at the same time. Then it is single-threaded execution. Therefore, its order can be guaranteed.

synchronized and lock optimization

The usage, principle and effect on concurrent programming of synchronized were introduced earlier. is a great keyword to use.

synchronized is actually implemented with the help of Monitor. The enter method of objectMonitor will be called when locking, and the exit method will be called when unlocking. In fact, only before JDK1.6, the implementation of synchronized will directly call enter and exit of ObjectMonitor. This kind of lock is called a heavyweight lock.

Therefore, in JDK1.6, a lot of optimizations have been made on locks, and then there are lightweight locks, biased locks, lock elimination, adaptive spin locks, and lock coarsening (spin locks exist in 1.4, but the default is turned off, jdk1.6 is turned on by default), these operations are to share data more efficiently between threads and solve competition problems.

Well, regarding the synchronized keyword, we introduced its usage, principle, and how to ensure atomicity, sequence, and visibility, and also expanded the information and thoughts related to lock optimization . We will continue to introduce the volatile keyword and the difference between it and synchronized later. Stay tuned.

Message of the day

Youth and love will never grow old, even if you go through thorns and thorns and lose your angry horse and fresh clothes. –Mo Jun

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Java skill tree concurrencyThe definition of concurrency 108415 people are learning the system