4. Implementation principle of Synchronized

4.1 Synchronization

Synchronization in the Java Virtual Machine is implemented by monitor entry and exit, either explicitly (by use of the monitorenter and monitorexit instructions) or implicitly (by the method invocation and return instructions).


For code written in the Java programming language, perhaps the most common form of synchronization is the synchronized method. A synchronized method is not normally implemented using monitorenter and monitorexit. Rather, it is simply distinguished in the run-time constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions (§2.11.10).

  • The Java® Virtual Machine Specification 3.14. Synchronization
  • In the JVM, synchronization is implemented through entry and exit of monitor locks, either explicitly through monitorenter and Monitorexit directives, or implicitly through method calls and return directives
  • Perhaps the most common implementation of synchronization for Java code is the synchronization method. The synchronization blocks are implemented using Monitorenter and Monitorexit, while the synchronization method is implicitly implemented using the ACC_SYNCHRONIZED flag. The idea is to check whether the method contains the ACC_SYNCHRONIZED tag in the constant pool with a method invocation instruction
  • This article will not analyze the bytecode implementation of Synchronized, but only briefly. Interested readers can refer to the JVM source code analysis of Synchronized implementation (of course, if the opportunity to open JVM, I will re-analyze).

4.2 decompiling

2 prepared

To get an idea of how Synchronized works, decomcompile the SynchronizedDeme class’s class file and see what happens

package concurrent;
public class SynchronizedDemo {
    public static synchronized void staticMethod() throws InterruptedException {
        System.out.println("Static synchronization method begins");
        Thread.sleep(1000);
        System.out.println("End of statically synchronized method");
    }
    public synchronized void method() throws InterruptedException {
        System.out.println("Instance synchronization method started");
        Thread.sleep(1000);
        System.out.println("Instance synchronization method ends");
    }
    public synchronized void method2() throws InterruptedException {
        System.out.println("Instance synchronization method 2 begins");
        Thread.sleep(3000);
        System.out.println("Instance synchronization method 2 ends"); } public static void main(String[] args) { final SynchronizedDemo synDemo = new SynchronizedDemo(); Thread thread1 = new Thread(() -> { try { synDemo.method(); } catch (InterruptedException e) { e.printStackTrace(); }}); Thread thread2 = new Thread(() -> { try { synDemo.method2(); } catch (InterruptedException e) { e.printStackTrace(); }}); thread1.start(); thread2.start(); }}Copy the code

4.2.1 Generating. Class Files

javac SynchronizedDemo.java 
Copy the code

Note: Since the default encoding of the author’s OS is UTF-8, the following error may occur

The solution is as follows: Just specify -encoding to specify the encoding mode

javac -encoding UTF-8 SynchronizedDemo.java 
Copy the code

We end up with a.class file, synchronizedDemo.class

4.2.2 JavAP Decompilation

javap -v SynchronizedDemo 
Copy the code

Decompilation gives us different compilation results for constant pools, synchronized methods, and synchronized code blocks, which we will cover later

Constant pool diagram

In addition to primitive types and constant values for strings and arrays, the constant pool also contains symbolic references in text form:

  • Fully qualified names of classes and interfaces
  • The name and descriptor of the field
  • Method and name and descriptor

Synchronization method diagram

The synchronization method contains an ACC_SYNCHCRONIZED identifier

Synchronized code block icon

The synchronized code block inserts monitorenter and Monitorexist directives into the code

4.3 Synchronizing Code Blocks Synchronizing principles

4.3.1 Monitor Monitor

  • Each object has a monitor, and the JVM acquires and releases synchronization locks through the Monitorenter and Monitorexist directives in the synchronization block
  • When a thread acquires a synchronous lock, it acquires the lock by acquiring the Monitor
  • The implementation of Monitor is similar to that of a tube in an operating system

4.3.2 monitorenter instruction

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:


• If the entry count of the monitor is associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.


• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.


• If another thread already owns the monitor with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

  • Each object has a monitor. The monitor is locked when it is occupied (or a synchronization lock is acquired when the monitor is acquired). When a thread executes the Monitorenter directive, it attempts to take ownership of the monitor as follows:
  • If the number of entries for the monitor is 0, the thread enters the monitor and sets the number of entries to 1, at which point the thread becomes the owner of the monitor
  • If the thread already owns the monitor and re-enters, the number of entries is +1
  • If the monitor is already owned by another thread, the thread is blocked until the number of monitor entries reaches zero, after which the threads compete for ownership of the monitor
  • Only the thread that first acquired the lock is allowed to continue to acquire multiple locks

4.3.3 monitorexit instruction

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.


The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

  • Executing the Monitorexit directive follows the following steps:
  • The thread executing the Monitorexit directive must be the owner of the monitor to which the object instance corresponds
  • When the instruction executes, the thread will first change the number of entries to -1. If the number of entries after -1 becomes 0, the thread will exit the monitor (that is, release the lock).
  • Other threads that block on the monitor can recontest ownership of the monitor

4.3.4 Implementation Principle

  • In the synchronization block, the JVM implements the acquisition and release of synchronization locks through the Monitorenter and Monitorexist directives
  • The Monitorenter directive is inserted at the beginning of the synchronized code block after compilation
  • The Monitorexit directive inserts at the end of the method and at the exception
  • The JVM ensures that each Monitorenter must have a Monitorexit paired with it
  • Any object has a Monitor associated with it, and when a Monitor is held, it is locked
  • When a thread executes the Monitorenter directive, it attempts to acquire ownership of the object’s monitor, that is, the lock on the object
  • When the thread executes monitorexit, it releases the monitor with an entry count of -1 until it becomes 0
  • Only one thread can succeed at a time, and other threads that fail are BLOCKED and put into a synchronized queue, where they are BLOCKED

4.3.4 supplement

  • See How locks are used for objectref
  • Due to wait/notify such methods as the underlying implementation is based on the monitor, so only in the synchronous method (block) can call wait/notify method, otherwise will be thrown. Java lang. The cause of the exception IllegalMonitorStateException

4.4 Synchronization Methods Synchronization principles

  • Unlike the monitor implementation of synchronized code blocks, synchronized methods are implicitly implemented using the ACC_SYNCHRONIZED flag
  • The principle is to check whether the method contains the ACC_SYNCHRONIZED flag in the constant pool with a method invocation instruction, and if so, the JVM requires the thread to request a lock before invoking it

5. The principle of progression

5.1 Monitor Obejct mode

5.1.1 Monitor Obejct Model overview

  • Monitor is a synchronization tool, or a synchronization mechanism. It is usually described as an object, characterized by mutual exclusion and signaling
  • Mutually exclusive: A Monitor lock can be held by only one thread at a time. No other thread can hold it
  • Signal: A thread that fails to hold a Monitor lock will temporarily give up contention and wait for a predicate to come true (condition variable), but when the condition is true, the current thread will notify other threads that are waiting for the condition variable by releasing the lock so that they can recontest the lock

Signal mechanism of Mesa

  • Mesa signal is a Blocking condition variable
  • When a thread that holds a Monitor lock issues a release notice, it does not immediately lose the lock, but lets other threads wait in the queue and recontest the lock
  • In this mechanism, after the waits get the lock, they cannot determine whether other waits have entered Monitor in this time difference. Therefore, the predicate cannot be guaranteed to be true, so they must use while to judge the condition
  • Java uses Mesa’s Singal mechanism, known as Notify

5.1.2 Monitor Obejct mode architecture

In the Monitor Object schema, there are four main types of participants:

5.1.3 Monitor Obejct Mode collaboration process

1. Synchronous method invocation and serialization:

  • When a client thread calls a synchronous method on a monitor object, it must first acquire its monitor lock
  • The fetch operation will not succeed as long as there are other synchronized methods being executed on the monitor object
  • When the monitor object is occupied by a thread (that is, the synchronized method is being executed), the client thread is blocked until it acquises the monitor lock
  • When the client thread successfully acquires the monitor lock, it enters the critical section and executes the service implemented by the method
  • Once the synchronized method completes execution, the monitor lock is automatically released to give other client threads the opportunity to call the synchronized method executing the monitor object

2. Synchronous method thread suspends: If the client thread calling the synchronous method must be blocked or cannot proceed immediately for some other reason, it can wait on a Monitor Condition, which causes the client thread to temporarily release the Monitor lock and be suspended on the Monitor Condition

3. Monitoring condition notification: A client thread can notify a monitoring condition in order to notify the thread blocking on the monitoring condition (the monitoring lock) to resume running

4. Synchronization method thread recovery:

  • Once a synchronous method thread that was previously suspended on the monitoring condition gets the notification, it continues execution at the original point where the monitoring condition was awaited
  • Monitor locks are acquired automatically (threads automatically compete for locks) before notified threads are allowed to resume executing synchronous methods

The author will further elaborate on Monitor in ReentractLock

5.2 object head

5.2.1 Objects in JVM memory

  • In the JVM, the layout of objects in memory is divided into three areas: object headers, sample data, and aligned padding
  • Object header: The object header mainly stores the object’s hashCode, lock information, type pointer, array length (if array), and so on
  • Example data: Stores the attribute data of the class, including the attribute information of the parent class, and the array length if it is the instance part of the array, which is aligned by 4 bytes
  • Populate data: Since the JVM requires that the object’s starting address be an integer multiple of 8 bytes, populate automatically when 8 bytes are not met (so populate data is not required, just for byte alignment)

5.2.2 Overview of object headers

  • Synchcronized locks are stored in the header of a Java object
  • The JVM stores the object header with 3 subwidths (Word) if the object is an array type, and 2 subwidths otherwise
  • In a 32-bit VM, a sub-width is 4 bytes, that is, 32 bits. 64-bit means 8 bytes, or 64bit

5.2.3 Storage structure of Mark Word

Default Mark Word storage structure for 32-bit JVMS (unlocked state)

During runtime, the data stored in Mark Word changes with the lock flag bit (32 bits).

Default Mark Word storage structure for 64-bit JVMS (25 bits not used for 32-bit lockless state)

5.3 Monitor Record

5.3.1 Monitor Record Overview

  • Monitorrecords (Mrs) are private data structures for Java threads. Each thread has a list of available Mrs, as well as a global list of available ones
  • A locked object is associated with a MR (the LockWord in the MarkWord of the object header points to the starting address of MR)
  • An Owner field in MR holds the unique identity of the thread that owns the lock, indicating that the lock is occupied by that thread

5.3.2 Monitor Record Structure

5.3.3 Working mechanism of Monitor Record

  • A thread that successfully obtains a monitor lock becomes the owner of the monitor lock object
  • At any given time, the monitor object belongs to only one active thread.
  • The owner can call the wait method to automatically release the watch lock and enter the wait state

6. Optimize lock

6.1 the spin lock

  • Pain point: Since thread blocking/waking requires the CPU to switch between user and kernel states, frequent transitions place a heavy burden on the CPU, which in turn has a significant impact on concurrency performance
  • Symptom: Through extensive analysis, it has been found that the lock state of an object lock usually lasts only a short period of time, and there is no need to frequently block and wake up threads
  • How it works: By executing a meaningless empty loop to make a thread wait a certain amount of time without being immediately suspended, the thread holding the lock can release the lock quickly. If the lock is released quickly, the current thread has a chance to acquire the lock without blocking, thus reducing switching and improving performance
  • Pitfalls: Spin efficiency is good if the lock can be released quickly (the fewer spins actually performed, the better the efficiency, the lower the wait time); However, if the lock is held all the time, then the spin is not doing anything meaningful but is wasting CPU resources
  • Note: there must be a limit to the number of spins (or spin time). If you exceed the number of spins (time) without obtaining the lock, you will be blocked and suspended
  • -xx :+UseSpinning is enabled by default. The number of spins can be adjusted by using -xx :PreBlockSpin. The default number is 10

6.2 Adaptive spin lock

  • Pain point: Because the spin lock can only specify a fixed number of spins, but due to the difference in the task, the optimal number of spins for each time varies
  • Principle: By introducing the concept of “intelligent learning”, the number of spins is determined by the previous spin time on the same lock and the state of the lock holder. In other words, the number of spins is not fixed, but can be calculated by analyzing the last time, the next time, more intelligent
  • Implementation: If the current thread spins successfully for a lock, then the next spin may increase at this time (because the JVM considers this success to be the basis for the next success). Anyway, if the spin is rarely successful, then the number of spins will decrease (reduce idle waste) or even skip the spin process and block directly (since the spin is completely meaningless, it’s better to block directly).
  • Add: With adaptive spin locks, JVMS become smarter and more accurate at predicting lock conditions as application execution and performance monitoring information improves

6.3 blocking locks

6.3.1 blocking locks

  • Lock success: When a lock contention occurs, only the thread that acquired the lock can continue execution
  • Lock failure: A thread that fails to compete is blocked from running and placed in a wait queue associated with the target lock
  • Unlock: When the thread holding the lock exits the critical section, the lock is released and a blocked thread in the waiting queue is awakened to re-participate in the lock contention
  • Note: This article does not cover specific JVM models, but for those interested in HotSopt JVMS, check out the JVM locking mechanism 1-synchronized

6.3.2 fair lock

In terms of implementation, when a thread is competing for an object lock, as long as the waiting queue for the lock is not empty, the thread must be blocked and pushed to the end of the queue (the end of the queue is usually inserted through a CAS operation to ensure that no lock is released during the process of insertion).

6.3.3 Unfair Lock

On the other hand, in the scenario of unfair lock, each thread will compete for the lock first, and will be put into the waiting queue only when the competition fails or the current lock has been locked. In this implementation, the last thread may directly compete for the lock without entering the waiting queue (randomness).

6.4 lock coarsening

  • Pain point: Lock and unlock operation caused by multiple connections
  • Principle: The lock and unlock operations that are connected together for many times are merged into one time, and multiple consecutive locks are expanded into a larger range of locks
  • Use: to contract multiple synchronized blocks close to each other in a synchronized block or to combine multiple synchronized methods into one method
  • Add-on: In JDK apis, StringBuffer, Vector, and HashTable all have implicit locking operations and can be merged
/** * StringBuffer is a thread-safe string handling class. * Each call to stringBuffer.append requires locking and unlocking. If the virtual machine detects a series of locking and unlocking operations on the same object in a row, it combines them into a single, larger locking and unlocking operation. */ StringBuffer StringBuffer = new StringBuffer(); public voidappend(){
    stringBuffer.append("kira");
    stringBuffer.append("sally");
    stringBuffer.append("mengmeng");
}
Copy the code

6.5 lock elimination

  • Pain point: According to code escape techniques, a piece of code is considered thread-safe and does not need to be locked if it is determined that data on the heap will not escape from the current thread
  • How it works: The JVM saves money by eliminating unwanted locks by removing unnecessary lock operations at compile time by removing locks that cannot compete for shared resources by describing the running context
  • Use: Escape analysis and lock elimination can be enabled with the parameters -xx :+DoEscapeAnalysis and -xx :+EliminateLocks respectively (lock elimination must be in -server mode)
  • Note: In JDK apis such as StringBuffer, Vector, and HashTable, implicit locking can be eliminated
Public static void main(String[] args) {SynchronizedDemo SynchronizedDemo = new SynchronizedDemo();for (int i = 0 ; i < 10000 ; i++){
        synchronizedDemo.append("kira"."sally"); }} public void appEnd (String str1,String str2){// Because the StringBuffer object is wrapped inside the method, there can be no contention for shared resources. Delete lock operations at compile time. Knowing that there will be no thread-safety issues, StringBuilder should be used during the code phase otherwise StringBuffer will not be optimized without lock elimination enabled. StringBuffer StringBuffer = new StringBuffer(); stringBuffer.append(str1).append(str2); } / * *Copy the code

6.6 Upgrading locks

  • As of JDK1.6, there are four lock states: no lock, biased lock, light lock, and weight lock
  • The status of the lock escalates as the race progresses, and the lock allows escalation but not degradation
  • The purpose of disallowing degradation is to improve the efficiency of acquiring and releasing locks
  • I will explain this in reverse order: heavyweight -> Lightweight -> biased locks, because it is usually an optimization of the former

Lock upgrade process

6.7 Heavyweight Locks

  • Heavyweight locking is implemented through monitor inside objects (see Monitor Object schema above)
  • The essence of Monitor relies on the MutexLock implementation of the underlying operating system. The switching between threads implemented by the operating system is completed by switching between user mode and kernel mode, and the switching cost is very high
  • At the heart of MutexLock is the idea of trying to acquire locks. If available, possess. If not, go to sleep and wait
  • Interested readers can read a brief introduction to Mutex (Lock), which gives a good explanation of Liunx’s MutexLock

6.8 Lightweight Lock

6.8.1 Lightweight Locks Overview

  • Pain point: Since thread blocking/waking requires the CPU to switch between user and kernel states, frequent transitions place a heavy burden on the CPU, which in turn has a significant impact on concurrency performance
  • Main objective: To reduce the performance cost of traditional heavyweight locks using OS mutex without multithreading competition
  • Upgrade timing: When biased locks are disabled or multiple threads compete for biased locks, biased locks are upgraded to lightweight locks
  • Principle: Further improves performance when only one thread executes a synchronized block
  • Data structure: including Pointers to lock records in the stack, lock flag bits
  • Add: We recommend that you first read chapter 8 of “Into the JVM” about stack frames for the Virtual Machine bytecode execution engine

6.8.2 Lightweight Lock Flowchart

Thread 1 and thread 2 compete for the lock at the same time, causing the lock to swell into a heavyweight lock

6.8.3 Lightweight Locks Adding Locks

  • 1. JVM will create a space for storing lock records in the stack frame of the current thread before the thread executes the synchronized block and copy the product of the product header into the lock record (product is called the Mark Word)
  • 2. After the copy is successful, the thread attempts to use CAS to replace the Mark Word in the Object header with the pointer to the lock record (update the Mark Word in the Object header to the pointer to the lock record, and change the Owner pointer in the lock record to the Object Mark Word).
  • If the update succeeds, the current thread acquires the lock and continues to execute the synchronization method
  • If the update fails, other threads compete for the lock, and the current thread attempts to acquire the lock using spin. If the spin fails, the lightweight lock is upgraded to a heavyweight lock, and the current thread is blocked

6.8.4 Lightweight Lock Unlocking

  • When unlocking the product of herbier’s product, use CAS operation to replace the product with the object header.
  • If the unlock succeeds, no competition occurs
  • If the lock fails to be unlocked, it indicates that the current lock is in contention, and the lock expands to a heavyweight lock. When the lock is released, the blocked thread needs to wake up, and then the threads need to re-compete for the heavyweight lock according to the heavyweight lock rules

6.8.5 Precautions for Lightweight Locks

  • Hidden danger: For lightweight locks, there is a premise that “there is no multi-threaded race environment”. Once this premise is crossed, in addition to the cost of mutual exclusion, it will also increase the cost of additional CAS operation. In multi-threaded race environment, lightweight locks are even slower than gravity locks

6.9 biased locking

6.9.1 Overview of biased locking

  • Pain point: Hotspot authors find that in most cases there is no multi-thread contention, but the same thread obtains the same lock multiple times. In order to make the lock cheaper for the thread, they design biased locking (this is very related to business usage).
  • Main purpose: To minimize unnecessary lightweight lock execution paths without multithreading contention
  • Principle: Performance is further improved by adding token checks to reduce CAS operations when only one thread executes a synchronized block
  • Data structure: including the id of the thread that owns the lock, whether it is a biased lock, epoch(the timestamp of the biased lock), age of the object generation, lock flag bit

6.9.2 Flowchart of biased locking

Thread 1 demonstrates the initialization of a biased lock and thread 2 demonstrates the unlocking of a biased lock

6.9.3 Biased Lock Initialization

  • When a thread accesses the block and acquires the lock, it stores the thread ID in the lock record of the object header and the stack frame. In the future, the thread does not need to spend CAS to lock and unlock the block, but simply checks whether the thread is stored in the MarkWord of the object header:
  • If yes, the thread has obtained the lock and continues to execute the task
  • If no, you need to check whether the current lock is a biased lock (that is, whether the biased lock identifier in the object header is set to 1 and the lock identifier bit is set to 01) :
  • If it is not set, CAS is used to compete for the lock.
  • If set, try using CAS to point the bias lock of the object header to the current thread, which is the thread ID in the structure

6.9.4 Revoke a lock based on bias

  • Biased locking uses a mechanism that waits until a contention occurs to release the lock, and only when other threads compete for the lock will the thread holding the biased lock release the lock
  • Revocation of bias locks requires waiting for the global safe point (at which no bytecode is being executed)
  • To undo biased locks, follow these steps:

The thread with the bias lock is suspended and checked to see if the thread is alive:

  1. If the thread is inactive, set the object header to lock free (other threads will regain the bias lock)
  2. If the thread is active, the stack with the biased lock will be executed, iterating over the biased lock record and resetting the MarkWord for the lock record in the stack and the object header:
  • Or re-bias to another thread (giving the bias lock to another thread is equivalent to the current thread “being” released)
  • Either revert to no lock or mark the lock object as unsuitable as a bias lock (at which point the lock is upgraded to lightweight)

Finally, the suspended thread is woken up, and the thread blocked at the safe point continues to execute the synchronized code block

6.9.5 Biased Lock Closes the lock

  • Bias locking is enabled by default in JDK1.6 or higher and will be activated several seconds after the application starts
  • It is necessary to use the JVM parameters to close the delay – XX: BiasedLockingStartupDelay = 0
  • If it is determined that the lock is usually in contention, biased locking can be turned off with the JVM parameter -xx: -usebiasedLocking =false, and lightweight locking is entered by default

6.9.6 Precautions for biased locking

  • Advantage: Biased locks only need to rely on the CAS atomic instruction once when the ThreadID is replaced, and no CAS instruction is required at all times (compared to other locks)
  • Hidden danger: Since biased locks must be revoked in the event of multithreaded contention, the performance loss of biased lock cancellation must be less than the performance cost of the saved CAS atomic instruction (which is usually only known through extensive pressure measurement).
  • Comparison: Lightweight locking is designed to improve performance when threads alternately execute synchronized blocks, while biased locking is designed to further improve performance when only one thread executes synchronized blocks

6.10 Biased Locks vs. Lightweight locks vs. Heavyweight Locks


Welcome to Zhihu columnKeeping up with Java8, share excellent Java8 Chinese guides and tutorials, and welcome to contribute high-quality articles.


Synchronized (version 1.8)
by
Daniel huang, kira
Create, adopt
Creative Commons Attribution – International License for Non-Commercial Use 4.0Grant permission.