[concept analysis] on the principle and use of Java keyword synchronized

Keywords: Java

Introduction to preface

When it comes to synchronized in Java, many people's first reaction is lock, which is inaccurate. In Chinese, it means synchronization. Lock is a concept, an abstract noun, and synchronization is an action and an operation result.

Inaccurate translation leads to deviation in understanding. For example, the translation of Robust into Chinese means Robust, which is also a feature we talked about when learning Java, but some places are transliterated as "robustness". If someone asks you if you look confused, there is also a parental delegation mechanism, so I won't say more here.

When it comes to synchronization, you will definitely think of asynchrony. Here we will not extend the concept. Synchronization is to ensure orderly execution. Multithreading is a means to achieve asynchrony, not the ultimate goal. There is no equivalence between the two.

In Java, for multi-threaded concurrent operations, considering the safety of memory variables, synchronous blocking and orderly operation are required. Variable safety does not mean unchanged, but orderly variable.

Here is a brief introduction to the JMM (Java Memory Model) Java Memory Model, which is helpful to understand the actual scenario. Due to different hardware implementations, each manufacturer has its own implementation, such as Intel's MESI (cache consistency protocol). In order to shield various production differences, Java defines a specification to make the running effect of Java programs consistent on all platforms, which is also the reason why Java supports cross platforms. JMM stipulates:

  • All variables are stored in main memory, including instance variables and static variables, excluding local variables and method parameters.
  • Each thread has its own working memory. The working memory of the thread stores the variables used by the thread and a copy of the main memory. The operation of the thread on the variables is carried out in the working memory.
  • Different threads cannot access variables in each other's working memory. The transfer of variable values between threads needs to be completed through main memory.
  • Threads cannot directly read or write variables in main memory.

Sketch Map:

Starting from this specification, it is not difficult to understand the meaning of synchronized and volatile keywords, as well as the use of ThreadLocal and thread internal TLAB. In general, JMM defines atomicity, ordering and visibility, which is the basis of Java concurrency.

Principle analysis

synchronized

In Java, the most basic way of mutually exclusive synchronization is to use synchronized to modify a piece of code or method, and ensure the order of code execution by locking the reference of an object.

Object memory layout

synchronized locks in Java are objects. To clearly understand the principle of lock implementation, we must first clarify how objects in Java are composed. The memory layout for Hotspot virtual machine objects in Java includes three parts

  • Object Header
  • Instance Data
  • Align Padding

The field information contained in the instance data bit object, which is not necessary to align and fill bits, will not be analyzed in detail here.

Object header analysis

The object header mainly contains the runtime data of the object itself, mainly including mark word and klass pointer. The length of mark word in 32-bit and 64 bit virtual machine implementation is 32 bits and 64 bits respectively.

  • mark word, which stores synchronization status, identification, hashcode, GC status, etc;
  • klass pointer, which stores the type pointer of the object to judge which class instance it is.

The storage contents of the object header are as follows:

In the 32-bit virtual machine, the object is not locked synchronously. The space used is mainly 25bit to store hash code, 4bit to store the generation age of the object, 2bit to store the lock flag bit, and 1bit is fixed to 0. The object storage format is as follows

// Bit-format of an object header (most significant first, big endian layout below):
//
//  32 bits:
//  --------
//  hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//  size:32 ------------------------------------------>| (CMS free block)
//  PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

In a 64 bit virtual machine, the JVM will use the option + UseCompressedOops by default to enable pointer compression. Compressing the pointer to 32 bits can reduce memory usage by 50%. The object storage contents are as follows:

|--------------------------------------------------------------------------------------------------------------|
|                                              Object Header (128 bits)                                        |
|--------------------------------------------------------------------------------------------------------------|
|                        Mark Word (64 bits)                                    |      Klass Word (64 bits)    |       
|--------------------------------------------------------------------------------------------------------------|
|  unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object   |  No lock
|----------------------------------------------------------------------|--------|------------------------------|
|  thread:54 |         epoch:2      | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object   |  Bias lock
|----------------------------------------------------------------------|--------|------------------------------|
|                     ptr_to_lock_record:62                            | lock:2 |     OOP to metadata object   |  Lightweight lock
|----------------------------------------------------------------------|--------|------------------------------|
|                     ptr_to_heavyweight_monitor:62                    | lock:2 |     OOP to metadata object   |  Weight lock
|----------------------------------------------------------------------|--------|------------------------------|
|                                                                      | lock:2 |     OOP to metadata object   |    GC
|--------------------------------------------------------------------------------------------------------------|

Meaning of each part:

  • lock: lock status flag bit. The value of this flag is different, and the meaning of the whole mark word is different.
  • biased_lock: bias lock flag. When it is 1, it means that the bias lock is enabled for the object, and when it is 0, it means that the object has no bias lock.
  • Age: Java GC tag bit object age.
  • identity_hashcode: object identification Hash code, using delayed loading technology. When the object is calculated using HashCode(), the result will be written to the object header. When the object is locked, the value is moved to thread Monitor.
  • Thread: the thread ID and other information that holds the biased lock. This thread ID is not the thread ID number assigned by the JVM. It is the same as the ID in Java Thread.
  • epoch: biased timestamp.
  • ptr_to_lock_record: pointer to the lock record in the stack.
  • ptr_to_heavyweight_monitor: pointer to thread monitor.

For the lock status bit, the corresponding locks of different flags are:

biased_lock lock state
0 01 No lock
1 01 Bias lock
0 00 Lightweight Locking
0 10 Heavyweight lock
0 11 GC tag

JOL use

The full name of JOL is Java Object Layout. Java Object Layout can view the data information contained in an object, break your imagination of Java objects and speak with data. JOL is also very simple to use.
Introducing maven dependency

<!-- JOL rely on -->
<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.10</version>
</dependency>

Code example:

/**
 * TestJOL
 *
 * @author starsray
 * @since 2021-11-25
 */
public class TestJOL {

    public static void main(String[] args) {
        TestJOL jol = new TestJOL();
        System.out.println(ClassLayout.parseInstance(jol).toPrintable());
    }
}

Output results:

jol.TestJOL 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     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 first line corresponds to the lock status
    unused:1 | age:4 | biased_lock:1 | lock:2
    0 0000 0 01 means that the A object is in an unlocked state
  • The third line represents the klass pointer compressed into 32 bits by the pointer
  • The fourth line is the 1-byte boolean value of the A object attribute information we created
  • The fifth line represents the alignment field of the object. In order to round up the 64 bit object, the alignment field occupies 3 bytes and 24 bits

synchronized use cases

Synchronized is a block syntax in Java. The object parameters are explicitly specified in its implementation source code. If an object is specified, the lock held by the thread is the specified object. If no object is specified, it is necessary to determine whether the lock held by the thread is the current object instance or Class object instance according to the instance method or static method used by synchronized. The following are several scenarios:

  • Static method
    When synchronized modifies a static method, the lock object type held by the current thread is the object corresponding to Class.
    public synchronized static void test() {
    
    }
    
  • Example method
    When synchronized modifies the instance method or specifies that the object is this, the lock object type held by the current thread is the instance object of the class where the current code is located.
    public synchronized void test() {
    
    }
    
    Equivalent to
    public void test() {
        synchronized (this) {
    
        }
    }
    
  • Synchronous code block
    When the synchronized modifier code is faster than the specified object, the lock object type held by the current thread is the specified object.
    • Common object
      public class TestSync {
      
      	private Object o = new Object();
      
      	public void test() {
      		synchronized (o){
      
      		}
      	}
      }
      
    • class object
      public class TestSync {
      
      	public void test() {
      		synchronized (TestSync.class) {
      
      		}
      	}
      }
      

When using an object as a lock, you should pay attention not to use a String. A literal String is a constant pool copy that will exist. If it does not exist, it will be newly created and is variable. The object as a lock must first be guaranteed to be a strong reference type and the reference is immutable before it can be guaranteed to be the same lock.

In addition, synchronized is reentrant as a synchronization lock. If a program or subroutine can be safely executed in parallel, it is called reentrant or re reentrant; That is, when the subroutine is running, you can enter and execute it again.

/**
 * Test synchronization
 *
 * @author starsray
 * @since 2021-11-25
 */
public class TestSync {
    public static void main(String[] args) {
        SyncChild syncChild = new SyncChild();
        syncChild.synMethod();
    }
}

class SyncFather{
    public synchronized void synMethod(){
        System.out.printf("current thread : %s, Method : father %n",Thread.currentThread().getName());
    }
}

class SyncChild extends SyncFather{
    public synchronized void synMethod(){
        super.synMethod();
        System.out.printf("current thread : %s, Method : child %n",Thread.currentThread().getName());
    }
}

Output results:

current thread : main, Method : father 
current thread : main, Method : child 

In Java, a thread can obtain locks multiple times without locking, which also shows that the object lock in Java is obtained at the thread granularity, not every call.

Lock upgrade

Before JDK1.5, the lock implemented by the synchronized keyword is a heavyweight lock, which requires the intervention of the operating system to switch between user state and kernel state. Each execution will consume a lot of resources. This is optimized in the later JDK implementation. It is not directly transformed into a heavyweight lock, but there is a lock upgrade process. Therefore, according to different business scenarios, It is not necessarily worse than the ReentrantLock of the JUC package.

Lock upgrade process:

No lock - > biased lock - > lightweight lock - > heavyweight lock, and the order of lock upgrade is irreversible.

No lock

No thread competes for resources, and the thread does not obtain the state of object lock.

  • Example code:
    package jol;
    
    import org.openjdk.jol.info.ClassLayout;
    
    /**
     * TestNoSync
     *
     * @author starsray
     * @date 2021/11/25
     */
    public class TestNoSync {
    
    	public static void main(String[] args) {
    		TestNoSync noSync = new TestNoSync();
    		System.out.printf("current Thread : %s",Thread.currentThread().getName());
    		System.out.println(ClassLayout.parseInstance(noSync).toPrintable());
    	}
    }
    
  • Output content:
    current Thread : main
    jol.TestNoSync 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)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
    	 12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
  • analysis:
    From the output of the first line, the current object header is biased_lock:0 lock:01. It is unlocked.

Bias lock

In terms of the name, the key point of the bias lock is bias. When the lock object is obtained by the thread for the first time, the virtual opportunity sets the lock flag bit of the object head to 01 and the bias mode to 1, indicating that it enters the bias mode. At the same time, the thread ID obtained the lock is recorded in the mark word of the object head through CAS. If CAS succeeds, Threads holding biased locks will not perform synchronization operations when entering the synchronization block, which improves performance.
reference resources:

  • OpenJDK JOL bias lock example
  • Sample code
    package jol;
    
    import org.openjdk.jol.info.ClassLayout;
    
    /**
     * TestJOL
     *
     * @author starsray
     * @since 2021-11-25
     */
    public class TestJOL {
    	public static void main(String[] args) {
    		try {
    			Thread.sleep(5000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		TestJOL jol = new TestJOL();
    		System.out.println(ClassLayout.parseInstance(jol).toPrintable());
    	}
    }
    
  • Output results
    jol.TestJOL object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
    	  0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
    	  4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    	  8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
    	 12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
  • analysis:
    From the output of the first line, the current object header is biased_lock:1 lock:01 indicates that it is currently in a biased state.
    Why can the bias lock be obtained when the thread sleep > 5 seconds? The OpenJDK JOL example explains:
     This is the example of biased locking.
    
     In order to demonstrate this, we first need to sleep for >5 seconds
     to pass the grace period of biased locking. Then, we do the same
     trick as the example before. You may notice that the mark word
     had not changed after the lock was released. That is because
     the mark word now contains the reference to the thread this object
     was biased to.
    
    Why is it 5 seconds instead of other times? After JDK1.6, the bias lock is turned on by default, but during JVM startup class loading, the bias lock is loaded last, while in other class loading, the object initialization process takes about 4 seconds, so the bias lock can be loaded in 5 seconds. You can turn off delayed loading through startup parameters, Or open and close the deflection lock.
    //Closing delay opening bias lock
    -XX:BiasedLockingStartupDelay=0
    //No deflection lock
    -XX:-UseBiasedLocking 
    //Enable deflection lock
    -XX:+UseBiasedLocking
    

Lightweight Locking

Lightweight lock is a lock mechanism introduced by JDK1.6. The purpose is to reduce the performance consumption caused by the traditional operating system heavyweight lock without multi-threaded competition. When there is thread contention, lightweight locks will expand into heavyweight locks.

  • Locking process
    The synchronization object is not locked (the lock flag bit is in the "01" state). The virtual machine first establishes a space called Lock Record in the stack frame of the current thread to store the current copy of Mark Word of the lock object (Displaced Mark Word). Then, the virtual machine will use CAS operation to try to update the Mark Word of the object to a pointer to the Lock Record. If the update action is successful, it means that the thread has the lock of the object, and the lock flag bit (the last two bits of Mark Word) of the object Mark Word will change to "00", indicating that the object is in a lightweight locking state.

  • Unlocking process
    The unlocking process is also carried out through CAS operation. If the Mark Word of the object still points to the lock record of the thread, CAS operation is used to replace the current Mark Word of the object and the Displaced Mark Word copied in the thread. If it can be replaced successfully, the whole synchronization process will be completed smoothly; If the replacement fails, it indicates that another thread has tried to obtain the lock. It is necessary to wake up the suspended thread while releasing the lock.

Lightweight locks can improve program synchronization performance based on the rule of thumb that "for most locks, there is no competition in the whole synchronization cycle". If there is no competition, the lightweight lock can successfully avoid the overhead of using mutex through CAS operation; However, if lock contention does exist, in addition to the cost of mutex itself, there is an additional cost of CAS operation. Therefore, in the case of competition, lightweight locks will be slower than traditional heavyweight locks.

  • Lightweight Locking
    • Sample code
      package jol;
      
      import org.openjdk.jol.info.ClassLayout;
      
      import java.util.concurrent.CountDownLatch;
      
      /**
       * TestJOL
       *
       * @author starsray
       * @since 2021-11-25
       */
      public class TestJOL {
      
      	static CountDownLatch cd = new CountDownLatch(1);
      
      	public static void main(String[] args) throws Exception {
      		long s = System.currentTimeMillis();
      		Thread.sleep(5000);
      
      		TestJOL testJOL = new TestJOL();
      
      		Thread thread = new Thread(() -> {
      			synchronized (testJOL) {
      				System.out.println("---> new thread locking <---");
      				System.out.println(ClassLayout.parseInstance(testJOL).toPrintable());
      			}
      			cd.countDown();
      		});
      		thread.start();
      		thread.join();
      
      		synchronized (testJOL) {
      			System.out.println("---> main thread locking <---");
      			System.out.println(ClassLayout.parseInstance(testJOL).toPrintable());
      		}
      
      		long e = System.currentTimeMillis();
      		cd.await();
      		System.out.println(e - s);
      	}
      }
      
    • Output results
      ---> new thread locking <---
      jol.TestJOL object internals:
       OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      	  0     4        (object header)                           05 48 46 8f (00000101 01001000 01000110 10001111) (-1891219451)
      	  4     4        (object header)                           72 01 00 00 (01110010 00000001 00000000 00000000) (370)
      	  8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
      	 12     4        (loss due to the next object alignment)
      Instance size: 16 bytes
      Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
      
      ---> main thread locking <---
      jol.TestJOL object internals:
       OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      	  0     4        (object header)                           70 f3 ff a9 (01110000 11110011 11111111 10101001) (-1442843792)
      	  4     4        (object header)                           39 00 00 00 (00111001 00000000 00000000 00000000) (57)
      	  8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
      	 12     4        (loss due to the next object alignment)
      Instance size: 16 bytes
      Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
      
      7403
      
  • Heavyweight lock
    • Sample code
      package jol;
      
      import org.openjdk.jol.info.ClassLayout;
      
      import java.util.concurrent.CountDownLatch;
      
      /**
       * TestJOL
       *
       * @author starsray
       * @since 2021-11-25
       */
      public class TestJOL {
      
      	static CountDownLatch cd = new CountDownLatch(1);
      
      	public static void main(String[] args) throws Exception {
      		long s = System.currentTimeMillis();
      		Thread.sleep(5000);
      
      		TestJOL testJOL = new TestJOL();
      
      		Thread thread = new Thread(() -> {
      			synchronized (testJOL) {
      				System.out.println("---> new thread locking <---");
      				System.out.println(ClassLayout.parseInstance(testJOL).toPrintable());
      			}
      			cd.countDown();
      		});
      		thread.start();
      		// thread.join();
      
      		synchronized (testJOL) {
      			System.out.println("---> main thread locking <---");
      			System.out.println(ClassLayout.parseInstance(testJOL).toPrintable());
      		}
      
      		long e = System.currentTimeMillis();
      		cd.await();
      		System.out.println(e - s);
      	}
      }
      
    • Output results
      ---> main thread locking <---
      jol.TestJOL object internals:
       OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      	  0     4        (object header)                           9a a6 cc ad (10011010 10100110 11001100 10101101) (-1379096934)
      	  4     4        (object header)                           83 01 00 00 (10000011 00000001 00000000 00000000) (387)
      	  8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
      	 12     4        (loss due to the next object alignment)
      Instance size: 16 bytes
      Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
      
      ---> new thread locking <---
      jol.TestJOL object internals:
       OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      	  0     4        (object header)                           9a a6 cc ad (10011010 10100110 11001100 10101101) (-1379096934)
      	  4     4        (object header)                           83 01 00 00 (10000011 00000001 00000000 00000000) (387)
      	  8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
      	 12     4        (loss due to the next object alignment)
      Instance size: 16 bytes
      Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
      
      7367
      
  • analysis
    From the above example, the main Thread delays loading, and the new Thread obtains the bias lock. After exiting the code block, there is still a competitive relationship with the main Thread. At this time, the main Thread obtains the lightweight lock. In example code 2, let two threads compete to obtain the heavyweight lock. The execution time of the whole process is compared with that of example code 1, The process of obtaining heavyweight locks will occur, but the execution time will be slightly lower than that of lightweight locks plus CAS (to be verified here).

Spin lock

Before upgrading a lightweight lock to a heavyweight lock, the thread executes the monitorenter instruction and enters the EntryList queue of the Monitor object. At this time, it will try to obtain the lock through spin. If the spin times exceed a certain threshold (default 10), it will be upgraded to a heavyweight lock and wait for the thread to be called.

The process of thread waiting arousal involves the switching between user state and kernel state of Linux system. This process is very resource consuming. The introduction of optional lock is to solve this problem. First, do not let the thread enter the blocking state immediately, but give it a chance to spin and wait.

The spin process is short. If the spin fails, it will expand into a key level lock through the lightweight lock. However, if too many threads enter the spin state, it will increase the CPU load. Therefore, the scenario with frequent thread switching is more suitable for synchronized. The JVM optimizes the spin scenario:

  • If the average load is less than CPUs, spin all the time
  • If more than (CPUs/2) threads are spinning, then the threads will block directly
  • If the spinning thread finds that the Owner has changed, it delays the spin time (spin count) or enters blocking
  • If the CPU is in power saving mode, stop spinning
  • The worst case of spin time is the storage delay of CPU (CPU A stores a data, and CPU B knows the direct time difference of this data)
  • The difference between thread priorities is appropriately discarded when spinning

Heavyweight lock

The heavyweight lock requires the intervention of the operating system and is implemented through mutual exclusion, which will consume too much performance. If the process mentioned above cannot be avoided, it will eventually be upgraded to the heavyweight lock.
The lock upgrade process in Java is irreversible. Once a heavyweight lock is obtained, the lock obtained will not be released until the current thread executes the synchronization code block.

Lock optimization

In the development process of Java, from directly calling the system heavyweight lock of JDK1.5 to the subsequent optimization, and introducing the lock upgrade process, these are implemented by the JVM. If synchronization is needed in daily development, you can also optimize the use of locks from coding details.

Lock coarsening

In the development process, in principle, the synchronized scope is generally required to be as specific as possible to reduce the scope of action. However, in some continuous operations, an object is frequently locked and unlocked. For example, in the loop body, this principle will be imprisoned.

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        synchronized (TestSync.class) {
            System.out.println("test");
        }
    }

    synchronized (TestSync.class) {
        for (int i = 0; i < 10; i++) {
            System.out.println("testa");
        }
    }
}

Lock elimination

Lock elimination means that when the Java virtual machine is running, the real-time compiler considers that it is impossible to eliminate locks with data sharing. The main judgment basis for lock elimination is the data support from escape analysis. If it is judged that all data on the heap in the code block will not escape and will be accessed by other threads, this part of data can be allocated on the stack, It is considered to be thread private, so synchronous locking is not necessary.
For example, the following code:

public class TestSync {

    public static String test(String s1, String s2, String s3) {
        return s1 + s2 + s3;
    }

    public static void main(String[] args) {
        System.out.println(test("1", "2", "3"));
    }
}

String is an immutable class. Before JDK1.5, for the continuous addition of strings, the compiled bytecode uses StringBuffer for append() operation, and the append() operation of StringBuffer is a thread safe method. Each append() operation is a synchronization block with the current object as the lock. The virtual machine analyzes the variables, Judge that the synchronization block is limited to the method test(), and the variables in it will not be accessed and shared by other threads. Therefore, after JIT compilation, each synchronization execution will be ignored to eliminate the performance overhead caused by locks.

Lock implementation

Synchronized in Java is more called lock, which is actually a synchronization structure. This synchronization structure is realized with the help of pipe Monitor. The method level synchronization is implicit and does not need to be controlled by bytecode. The virtual machine receives ACC from the method table structure in the method constant pool_ The synchronized access flag is used to determine whether a method is a synchronous method. If it is set, the execution thread can execute the method only if it holds the pipe first. During execution, other threads cannot obtain the pipe, so they cannot execute the method.

The Java virtual machine instruction set determines the beginning and end of the code block through the monitorenter and monitorexit keywords. The Java virtual machine specification defines that when executing the monitorenter instruction, you must first try to obtain the lock of the object. If the object is not locked or the current thread already holds the lock of that object, the value of the lock counter will be increased by one, and the value of the lock counter will be decreased by one when the monitorexit instruction is executed. Once the counter value is zero, the lock is released. If the acquisition of an object lock fails, the current thread should be blocked and wait until the object requesting the lock is released by the thread holding it.

Test code:

package jol;

/**
 * Synchronous bytecode
 *
 * @author starsray
 * @since 2021-11-28
 */
public class SyncByteCode {
    public void test() {
        synchronized (this) {
            System.out.println("test sync bytecode");
        }
    }

    public static void main(String[] args) {
        SyncByteCode byteCode = new SyncByteCode();
        byteCode.test();
    }
}

Bytecode:

// access flags 0x1
  public test()V
    TRYCATCHBLOCK L0 L1 L2 null
    TRYCATCHBLOCK L2 L3 L2 null
   L4
    LINENUMBER 11 L4
    ALOAD 0
    DUP
    ASTORE 1
    MONITORENTER
   L0
    LINENUMBER 12 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "test sync bytecode"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L5
    LINENUMBER 13 L5
    ALOAD 1
    MONITOREXIT
   L1
    GOTO L6
   L2
   FRAME FULL [jol/SyncByteCode java/lang/Object] [java/lang/Throwable]
    ASTORE 2
    ALOAD 1
    MONITOREXIT
   L3
    ALOAD 2
    ATHROW
   L6
    LINENUMBER 14 L6
   FRAME CHOP 1
    RETURN
   L7
    LOCALVARIABLE this Ljol/SyncByteCode; L4 L7 0
    MAXSTACK = 2
    MAXLOCALS = 3

Usage scenario

Singleton mode

Singleton mode is the simplest and most common design mode. Here, double check and synchronized are used to ensure thread safety, and volatile keyword is used to ensure the visibility between threads. Secondly, class loading mechanism may be adjusted during CPU disordered execution to optimize the initialization and assignment process, prohibit instruction reordering and ensure concurrency safety.

package jol;

/**
 * Singleton mode
 *
 * @author starsray
 * @since 2021-11-28
 */
public class Singleton {

    private static volatile Singleton INSTANCE;


    private Singleton() {

    }

    /**
     * Get instance
     *
     * @return {@link Singleton }
     */
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

summary

At the beginning of the design, synchronized is directly involved through the operating system, uses mutex as the weight to unlock, and the later optimized lock upgrade process shows that if you want to be familiar with and master how to use the lock well, you must deeply understand the details of its internal implementation. With the continuous progress of technology and the gradual reduction of use cost, the exploration of knowledge can not stop. Finally, let's summarize.

  • synchronized is an important way for Java to implement synchronization. This lock is reentrant.
  • JDK optimizes the synchronized lock upgrade. The lock upgrade process is irreversible. However, JIT will optimize the synchronization block for lock coarsening or lock elimination. Coding optimization can also be carried out according to the scene in daily coding.
  • Object memory layout. The object header records the basic information of the lock and holds thread information. The header information of 32-bit virtual machine is different from that of 64 bit virtual machine.
  • JMM defines the top-level specifications of irrelevant operating systems such as thread safety and memory safety, and shields the differences brought by hardware implementations such as the operating system.

Reference article:

Posted by loweauto on Sun, 28 Nov 2021 06:50:17 -0800