preface

Java concurrent programming series second Synchronized, article style is still illustrated, easy to understand, this article takes readers from simple to deep understanding of Synchronized, let readers also can and interviewers crazy line.

Synchronized has long been an elder statesman in concurrent programming. Prior to Jdk 1.6, it was referred to as a heavyweight Lock, and it was cumbersome compared to the locks provided by the J, U, and C packages. Synchronized performance is already very fast.

Outline of the content

Synchronized usage

Synchronized is a synchronization keyword provided by Java. In multi-threaded scenarios, read and write operations on code segments of shared resources (the write operation must be included. Optical reading does not have thread safety problems, because the read operation naturally has thread safety characteristics), and thread safety problems may occur. Synchronized can be used to lock out shared resource sections of code to achieve mutualexclusion and ensure thread safety.

Shared resource code segments are also called critical sections to ensure that critical sections are mutually exclusive. This means that only one thread can execute a critical section, and other threads block and wait to queue.

Synchronized can be eaten in three ways

  • Modify normal functions, monitor locks (monitor) is the object instance (this)
  • Modify static static functions, viewlock (monitorIs of the objectClassInstance (only one per objectClassInstance)
  • Modify code block, monitor lock (monitor) is the specified object instance

Common function

Ordinary functions use Synchronized simply by placing Synchronized between the access modifier and the function return type.

In multi-threaded scenarios, thread and threadTwo execute the INCR function, and the INCR function is read and written by multiple threads as a code segment of shared resource, which is called the critical region. In order to ensure the critical region is mutually exclusive, Synchronized can be used to modify the INCR function.

public class SyncTest {

    private int j = 0;
    
    /** * increment method */
    public synchronized void incr(a){
        // Critical section code --start
        for (int i = 0; i < 10000; i++) {
            j++;
        }
        // Critical section code --end
    }

    public int getJ(a) {
        returnj; }}public class SyncMain {

    public static void main(String[] agrs) throws InterruptedException {
        SyncTest syncTest = new SyncTest();
        Thread thread = new Thread(() -> syncTest.incr());
        Thread threadTwo = new Thread(() -> syncTest.incr());
        thread.start();
        threadTwo.start();
        thread.join();
        threadTwo.join();
        // The final print is 20000. If synchronized is not used, thread-safety issues will occur and the output is uncertainSystem.out.println(syncTest.getJ()); }}Copy the code

The code is very simple, incr function modified by synchronized, the function logic is to sum J for 10000 times, two threads execute incr function, and finally output J result.

Is decorated synchronized function we referred to as “synchronization function, thread execution said before the synchronization function, need to get the monitor lock, hereinafter referred to as lock, acquiring a lock is successful to perform synchronization function, after the synchronization function, the thread will release the lock and notify awaken other threads for locks, failed to get the lock” is blocked and wait for a notice awaken this thread to get lock “, The synchronization function takes this as the lock, which is the current object. For example, the syncTest object is shown in the above code snippet.

  • threadthreadperformsyncTest.incr()before
  • threadthreadObtaining the lock successfully
  • threadthreadTwoperformsyncTest.incr()before
  • threadthreadTwoFailed to obtain the lock
  • threadthreadTwoBlock and wait to wake up
  • threadthreadaftersyncTest.incr().jAccumulated to10000
  • threadthreadRelease lock, wake up notificationthreadTwoThread acquisition lock
  • threadthreadTwoObtaining the lock successfully
  • threadthreadTwoaftersyncTest.incr().jAccumulated to20000
  • threadthreadTwoRelease the lock

Static functions

A static function, as its name implies, is a static function that uses the same Synchronized approach as a normal function. The only difference is that the object is not this, but Class.

The code snippet for multithreaded Synchronized modifiers is shown below.

public class SyncTest {

    private static int j = 0;
    
    /** * increment method */
    public static synchronized void incr(a){
        // Critical section code --start
        for (int i = 0; i < 10000; i++) {
            j++;
        }
        // Critical section code --end
    }

    public static int getJ(a) {
        returnj; }}public class SyncMain {

    public static void main(String[] agrs) throws InterruptedException {
        Thread thread = new Thread(() -> SyncTest.incr());
        Thread threadTwo = new Thread(() -> SyncTest.incr());
        thread.start();
        threadTwo.start();
        thread.join();
        threadTwo.join();
        // The final print is 20000. If synchronized is not used, thread-safety issues will occur and the output is uncertainSystem.out.println(SyncTest.getJ()); }}Copy the code

Java static resources can be directly called by the Class name, static resources do not belong to any instance object, it only belongs to the Class object, each Class has only one Class object in J V M, so the synchronous static function will use the Class object as the lock, subsequent lock acquisition, lock release process is the same.

The code block

Both ordinary functions and static functions introduced above have relatively large granularity, and the scope of the whole function is locked. Now, if you want to narrow the scope and configure it flexibly, you need to use code blocks, and use {} symbols to define the scope to Synchronized modification.

The following code defines syncDbData, which is a pseudo-synchronous data function. It takes 2 seconds, and the logic does not involve reading or writing shared resources (non-critical section). Synchronized is only used in different postures, one for functions and the other for code blocks.

public class SyncTest {

    private static int j = 0;


    /** * Synchronize library data, time-consuming, code resources do not involve shared resource read and write operations. * /
    public void syncDbData(a) {
        System.out.println("Db data synchronization starts ------------");
        try {
            // Synchronization takes 2 seconds
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Db data synchronization completed ------------");
    }

    // Increment method
    public synchronized void incr(a) {
        //start-- critical section code
        // Synchronize library data
        syncDbData();
        for (int i = 0; i < 10000; i++) {
            j++;
        }
        //end-- critical section code
    }

    // Increment method
    public void incrTwo(a) {
        // Synchronize library data
        syncDbData();
        synchronized (this) {
            //start-- critical section code
            for (int i = 0; i < 10000; i++) {
                j++;
            }
            //end-- critical section code}}public int getJ(a) {
        returnj; }}public class SyncMain {

    public static void main(String[] agrs) throws InterruptedException {
        //incr synchronizes method execution
        SyncTest syncTest = new SyncTest();
        Thread thread = new Thread(() -> syncTest.incr());
        Thread threadTwo = new Thread(() -> syncTest.incr());
        thread.start();
        threadTwo.start();
        thread.join();
        threadTwo.join();
        // The final print is 20000
        System.out.println(syncTest.getJ());

        //incrTwo Synchronized block execution
        thread = new Thread(() -> syncTest.incrTwo());
        threadTwo = new Thread(() -> syncTest.incrTwo());
        thread.start();
        threadTwo.start();
        thread.join();
        threadTwo.join();
        // The final print is 40000System.out.println(syncTest.getJ()); }}Copy the code

SyncDbData () can be executed concurrently or concurrently with multiple threads. Synchronization is a Synchronized Synchronized method that can be executed concurrently or simultaneously with multiple threads.

We use code blocks to narrow the scope, define the right critical section, and improve performance. We turn our attention to incrTwo synchronous block execution. IncrTwo synchronizes by modifying the code block, locking only the self-increasing code segment.

In addition to flexible control range, Synchronized () can also work cooperatively between threads. Since Synchronized () brackets can receive any Object as a lock, it can use wait, notify, notifyAll and other functions of Object. Synchronized is not recommended because the LockSupport utility class is a better choice.

  • Wait: The current thread pauses to release the lock
  • Notify: Release the lock and wake up the thread that called WAIT (if there are more than one threads)
  • NotifyAll: Releases the lock and wakes up all the threads that have called wait

Principle of Synchronized

public class SyncTest { private static int j = 0; /** * Synchronize library data, time-consuming, code resources do not involve shared resource read and write operations. * / public void syncDbData () {System. Out. Println (" db data start synchronization -- -- -- -- -- -- -- -- -- -- -- -- "); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } system.out. println("db data synchronization completed ------------"); } public synchronized void incr() {//start-- synchronized void incr(); for (int i = 0; i < 10000; i++) { j++; Public void incrTwo() {// syncDbData(); Synchronized (this) {//start-- start for (int I = 0; i < 10000; i++) { j++; }} public int getJ() {return j; }}Copy the code

In order to explore the principle of Synchronized, we decompilate the above code, output the decompilated results, and see how the underlying implementation (environment Java 11, Win 10 system).

Only incr and incrTwo functions are interceptedpublic synchronized void incr(a);
    Code:
       0: aload_0                                         
       1: invokevirtual #11                 // Method syncDbData:()V 
       4: iconst_0                          
       5: istore_1                          
       6: iload_1                                     
       7: sipush        10000               
      10: if_icmpge     27
      13: getstatic     #12                 // Field j:I
      16: iconst_1
      17: iadd
      18: putstatic     #12                 // Field j:I
      21: iinc          1.1
      24: goto          6
      27: return

  public void incrTwo(a);    
    Code:
       0: aload_0
       1: invokevirtual #11                 // Method syncDbData:()V
       4: aload_0
       5: dup
       6: astore_1
       7: monitorenter                     / / acquiring a lock
       8: iconst_0
       9: istore_2
      10: iload_2
      11: sipush        10000
      14: if_icmpge     31
      17: getstatic     #12                 // Field j:I
      20: iconst_1
      21: iadd
      22: putstatic     #12                 // Field j:I
      25: iinc          2.1
      28: goto          10
      31: aload_1
      32: monitorexit                      // Exit properly to release the lock
      33: goto          41
      36: astore_3
      37: aload_1
      38: monitorexit                      // Asynchronously exit to release the lock
      39: aload_3
      40: athrow
      41: return

Copy the code

Ps: If you are interested in the above instructions, please Google “JVM bytecode Instructions table”.

IncrTwo is block synchronization. In the decompile result, we find monitorenter and Monitorexit directives (acquire and release locks).

Monitorenter inserts at the beginning of the block and Monitorexit at the end of the block. J V M needs to ensure that each Monitorenter has a Monitorexit corresponding to it.

Any object is associated with a monitor lock, and the thread attempts to take ownership of the Monitor when executing the Monitorenter instruction.

  • ifmonitorThe entry number of is0, the thread entersmonitorAnd then set the entry number to1, the thread ismonitorThe owner of the
  • If the thread already owns itmonitor, re-enter, thenmonitorThe number of entries plus1
  • Threads executemonitorexit.monitorThe number of entries to -1, how many times has it been executedmonitorenter, and finally execute the corresponding number of timesmonitorexit
  • If another thread is already occupiedmonitor, the thread enters the blocking state untilmonitorIs 0, then try again to obtainmonitorThe ownership of the

If you look back at incr, incr is synchronized as a normal function. Although the monitorenter and Monitorexit directives are not seen in the decomcompiled results, the actual flow is the same as incrTwo, executed by Monitor, but in an implicit way. Finish with a flow chart.

Synchronized optimization

Various tweaks have been made to the Synchronized keyword since Jdk 1.5, and Synchronized has become faster than it used to be, which is why the official recommendation to use Synchronized is as follows.

  • Lock coarsening
  • Lock elimination
  • Lock escalation

Lock coarsening

The critical range of mutual exclusion should be kept as small as possible to minimize the number of synchronized operations, reduce the blocking time and, if there is a lock contention, allow the thread waiting for the lock to acquire the lock as quickly as possible.

However, locking and unlocking also consumes resources. A series of consecutive locking and unlocking operations may cause unnecessary performance loss. Lock coarsening is to “connect multiple consecutive locking and unlocking operations together” to expand a wider range of locks, avoiding frequent locking and unlocking operations.

J V M detects that a series of operations lock the same object (J ++ for 10000 times, no lock coarser 10,000 times), then J V M coarses the range of the lock to the outside of the series of operations (such as outside of the for loop), The sequence of operations requires only one lock.

Lock elimination

(when the Java virtual machine in the JIT compiler can be simple to understand for the first time a piece of code will be executed when compile, also known as instant compiled), run through the context of the scan, after the escape analysis (object is used in the function, may also be referenced external function, called the escape function) and remove lock there can be no Shared resource competition, Eliminating unnecessary locks in this way saves meaningless time.

The code uses Object as the lock, but the Object’s life cycle is only in the incrFour() function and is not accessed by other threads, so it is optimized out during compilation (where Object is not an escaped Object).

Lock escalation

In Java, every object has an object header, which consists of a Mark World, a pointer to a class, and an array length. For this article, we only need to care about Mark World, which records the object’s HashCode, generational age, and lock flag bits.

Mark World simplified structure

The lock state Store content Lock tag
unlocked Object hashCode, object generation age, whether biased lock (0) 01
Biased locking Biased thread ID, biased timestamp, object generation age, whether biased lock (1) 01
Lightweight lock Pointer to the lock record in the stack 00
Heavyweight lock A pointer to a mutex (heavyweight lock) 10

Readers need to know that lock upgrades are reflected in the Mark World section of the lock object header, meaning that the contents of the Mark World will change as the lock is upgraded.

Java1.5 introduced biased locking and lightweight locking in order to reduce the performance cost of acquiring and releasing locks. Synchronized upgrades are in the order of “no lock > biased locking > lightweight locking > heavyweight locking.

Biased locking

In most cases, the lock is always acquired multiple times by the same thread, and there is no multi-thread competition, so biased locking appears. Its goal is to reduce the consumption of acquiring locks and improve performance when only one thread executes synchronized code blocks (biased locking can be turned off by J V M parameter: -xx: -usebiasedLocking =false, the program enters the lightweight locking state by default.

Before a thread executes a synchronized code or method, it only needs to check whether the thread ID in the Mark Word of the object header is the same as the current thread ID. If so, it directly executes the synchronized code or method. The specific process is as follows

  • No lock state, whether the stored content is biased lock (0) “, lock identifier bit01
    • CASSets the current thread ID toMark WordStore contents
    • Whether the lock is biased0=> Whether the lock is biased1
    • Execute synchronized code or methods
  • Biased lock state, store content “whether biased lock (1), thread ID “, lock identifier bit01
    • Contrast threadIDConsistent, if synchronous code or methods are executed consistently, otherwise enter the following flow
    • If not,CASwillMark WordThe threadIDSet to the current threadID, execute the synchronized code or method, otherwise enter the following process
    • CASIf the setting fails, it proves that there is multi-thread competition, triggering the cancellation of biased lock. When the global safety point is reached, the thread of biased lock is suspended, and the biased lock is upgraded to lightweight lock, and then the execution continues at the safe point.

Lightweight lock

Lightweight locks consider scenarios where there are not many threads competing for the lock object and the lock is held for a short time. If the lock is released shortly after the block, the cost is not worth the cost. Therefore, do not block the thread and let it spin for some time to wait for the lock to be released.

When the current thread holds a biased lock, it is accessed by another thread, and the biased lock is upgraded to a lightweight lock. Other threads will try to acquire the lock through the form of spin without blocking, thus improving performance. There are two main situations for obtaining lightweight locks: (1) when the biased lock function is turned off; (2) Multiple threads compete for biased locks, so biased locks are upgraded to lightweight locks.

  • No lock state, whether the stored content is biased lock (0) “, lock identifier bit01
    • When bias locking is turned off
    • CASSets the pointer to the lock record in the current thread stackMark WordStore content
    • The lock identifier bit is set to00
    • Execute synchronized code or methods
    • When you release the lock, return itMark Wordcontent
  • Lightweight lock state, store content “thread stack lock record pointer”, lock identifier bit00(Threads that store content are “threads that hold lightweight locks.”)
    • CASSets the pointer to the lock record in the current thread stackMark WordStore content, set successful lightweight lock acquisition, execute synchronized block code or method, otherwise execute the following logic
    • Set failure, proving that there is a certain amount of competition in multi-threading, thread spin step operation, spin a certain number of times or failed, lightweight lock upgrade to heavyweight lock
    • Mark WordThe stored content is replaced by the heavyweight lock pointer, lock marker bit10

Heavyweight lock

Once lightweight locks swell, they are upgraded to heavyweight locks, which rely on the operating system’s MutexLock to implement and need to move from user to kernel mode, which is very expensive, which is why Synchronized was inefficient before Java1.6.

When upgrading to a heavyweight lock, the status value of the lock flag bit changes to 10. At this time, the content stored in Mark Word is the pointer to the heavyweight lock, and the threads waiting for the lock will enter the blocking state. The following is a simplified version of the lock upgrade process.

Good historical articles recommended

  • From shallow to deep CAS, Xiao Bai can also align with BAT interviewers
  • Small white also can understand the Java memory model
  • Nanny Level teaching, 22 images uncover ThreadLocal
  • Can process, thread and coroutine be confused? A penny for you to know!
  • What is thread safety? This article will give you insight

About me

Here is A star, a Java program ape who loves technology. The public account “program ape A Star” will regularly share the operating system, computer network, Java, distributed, database and other high-quality original articles. 2021, grow together with you on the road of Be Better! .

Thank you very much for your little brothers and sisters to see here, the original is not easy, the article can be helpful to pay attention to, point a like, share and comment, are support (don’t want white whoring)!

May you and I both go where we want to go. See you in the next article