Synchronized overview

The synchronized keyword provides an exclusive locking mode and is used to control mutually exclusive access of multiple threads to shared resources. It guarantees that only one thread is executing the code at a time, and it also guarantees memory visibility of shared variables.

  • Mutual exclusion: Only one thread is allowed to hold one object lock at a time to achieve mutually exclusive access to shared resources at a time.
  • Visibility: Ensures that changes made to a shared variable before the lock is released are visible to another thread that subsequently acquires the lock.

Synchronized acquisition and release lock by JVM, users do not need to display the acquisition and release lock, very convenient. However, when a thread attempts to acquire the lock, it will block until the lock is acquired.

In earlier versions, synchronized was a heavyweight lock and inefficient. However, starting from JDK1.6, various lock optimization techniques were introduced to synchronized from the JVM level, such as spin lock, adaptive spin lock, lock elimination, lock coarser, lightweight lock and biased lock, which greatly reduced the overhead of lock operation.

use

Synchronized can be used in two ways: method block and side.

Synchronized method block

When applied to a code block, parentheses can be the specified object or a Class object.

// Lock the specified object instance public voidtest1(){synchronized(this) {// ···}}Copy the code
// Lock is the specified class object public voidtest2(){synchronized(test.class){// ···}}Copy the code

Synchronized methods

When applied to a method, the current object instance is locked.

public synchronized void test3() {/ /...}Copy the code

For static methods, class objects are locked.

public synchronized static void test4() {/ /...}Copy the code

The principle of basis

The HotSpot object head

The HotSpot VIRTUAL machine object header is divided into two parts of information:

  • Mark Word: used to store runtime data of the object itself, such as hash codes,GCAge of generation, lock type, lock mark and other information, this part of the data in32And the64In the vm, are respectively3264 bit. It is the key to implement lightweight locking and biased locking.
  • Class Metadata Address: a pointer to the object type of the method area, with an extra section for the array length if it is an array.

Mark Word is designed as a flexible data structure to store more information, reusing its own storage space based on the state of the object. For example, in a 32-bit HotSpot VIRTUAL machine, objects in various states are stored as follows:

Monitor

Each Java object has a Monitor object associated with it, which is called a tube (Monitor lock). In the previous table, when the lock status is heavyweight, the pointer points to the starting address of the Monitor object. When a Monitor is held by a thread, it is locked. In the HotSpot VIRTUAL machine source implementation, the ObjectMonitor object has the following attributes:

  • _count: counter;
  • _owner: Point to holdObjectMonitorObject thread;
  • _WaitSet: waiting pool;
  • _EntryList: lock pool;

When multiple threads access synchronized code, they will first enter the _EntryList lock pool and block. When the thread obtains the object’s Monitor, it will point _owner to the current thread and increment the _count counter in Monitor by one. If the thread calls the wait method, the _owner is restored to NULL, the _count counter is reduced by one, and the thread enters the _WaitSet wait pool.

When the thread completes execution, the corresponding variable is reset so that other threads can acquire the Monitor lock.

Four kinds of state

Synchronized has four states: no-lock, bias, lightweight, and heavyweight. As competition for locks intensifies, the lock state is upgraded.

Implementation principle of synchronized

Synchronized method block

public class SynchronizedTest {
    public void test1() {synchronized (this) {// ···}}}Copy the code

Disassemble synchronizedtest. class using javap -c -v:

As you can see, insert the MonitoRenter directive at the beginning of the synchronized code block and the Monitorexit directive at the end, and ensure that each Monitorenter has a Monitorexit corresponding to it.

Synchronized obtains the lock through Monitor. When a thread executes a Monitorenter instruction, it attempts to acquire Monitor ownership. When the counter is 0, it is successfully obtained; Set the lock counter to 1 after fetching. Set the lock counter to 0 when the Monitorexit directive is executed. If the object lock fails to be acquired, the current thread blocks and waits until the lock is released by another thread.

Synchronized methods

public class SynchronizedTest {
    public synchronized void test1() {// ···}}Copy the code

Disassemble synchronizedtest. class using javap -c -v:

As you can see, synchronized methods are simply translated into ordinary method calls and return instructions. There are no specific instructions at the JVM bytecode level to implement synchronized modification methods.

However, in the method table of the Class file, the position of the ACC_SYNCHRONIZED flag in the flags field of the method is 1, indicating that the method is synchronized. When the method is executed, the thread holds the Monitor object.

The underlying optimization

JDK1.6 introduces a number of optimizations for locking, such as spin locking, adaptive spin locking, lock elimination, lock coarser, lightweight locking, biased locking, and other techniques to reduce the overhead of locking operations.

Spin-locking and adaptive spin

In the implementation of synchronous mutex, if the lock fails to obtain, the current thread will block, but the thread suspension and recovery need to be converted between the kernel state and user state, which has a great impact on the system performance. In many cases the locked state of shared data does not last very long and it is not worth switching threads.

A spin lock allows a thread to execute a busy loop (spin) when it requests a lock that shares data. If it can acquire the lock quickly, it prevents it from entering a blocking state.

Spin wait avoids the overhead of thread switching, but it requires multiple processors and consumes processor time. If the lock is held for too long, it will consume more resources. Therefore, the spin wait time must be limited, and the number of spins must not be too high. The default is 10 spins, which can be changed using the -xx :PreBlockSpin parameter.

In JDK1.6, adaptive spin locks are introduced, whose spin time is determined by the previous spin time on the same lock and the state of the lock owner.

Lock elimination

Lock elimination means that the virtual machine’s just-in-time compiler performs lock elimination at runtime if the code requires synchronization, but detection finds that there is no possibility of a shared data race.

Lock elimination is based on escape analysis. If it is determined that none of the data on the heap will escape in a piece of code, it can be considered thread-private and synchronized locking is not required.

Lock coarsening

If a series of consecutive operations repeatedly lock and unlock the same object, and even locking occurs in the body of the loop, frequent locking and unlocking can lead to unnecessary performance costs even if there is no data contention.

Lock coarsening means that if the virtual machine detects such a situation, the scope of lock synchronization is extended (coarsening) outside the entire sequence of operations.

Biased locking

Biased locking eliminates the entire synchronization without contention, which reduces the cost of acquiring the lock for the same thread. The idea is that the lock is biased in favor of the first thread that acquired it, and if the lock is not subsequently acquired by another thread, the thread that holds the biased lock will never need to synchronize again.

When the lock object is acquired by thread for the first time, the lock enters bias mode, and the structure of Mard Word also changes into bias lock structure. The lock flag bit is “01”, and the CAS operation is used to record the ID of the thread that obtained the lock in the Mark Word of the object. If the CAS operation is successful, the thread can enter the lock related synchronization block every time in the future without any synchronization operation.

Not suitable for multi-threaded situations where lock competition is fierce.

The bias state ends when another thread attempts to acquire the lock object. According to whether the lock object is currently in the locked state, undo bias to restore to the unlocked state or lightweight locked state.

Lightweight lock

Lightweight locks are in contrast to traditional locks implemented using operating system mutex. Biased locks run when a thread enters a synchronized block and is upgraded to a lightweight lock if a second thread joins the lock contention. It is suitable for scenarios where threads are executed alternately.

When the code enters the synchronization block, if the synchronization object is not locked (the Lock flag bit is in the “01” state), the virtual machine will first create a space called Lock Record in the stack frame of the current thread, which is used to store the current copy of the Lock object Mark Word. Here is a thread stack on the left and a lock object on the right:

The virtual machine then uses the CAS action to try to update the object’s Mark Word to a pointer to the Lock Record, and to point the owner pointer in the Lock Record to the object’s Mark Word. If the update succeeds, the thread owns the lock on the object, and the object’s Mark Word lock bit changes to “00”, indicating that the object is in a lightweight locked state. The multithreaded stack and object header states are as follows:

If the update operation fails, the virtual machine will first check whether the object Mark Word point to the current thread’s stack frame, if have to explain the current thread already has the lock of the object, then you can directly enter the synchronized block to continue, if there is no point to explain the lock object has been preempted by other objects.

If there are more than two threads competing for the same lock, the lightweight lock expands to the heavyweight lock, and the lock flag changes to “10”. The pointer to the heavyweight lock is stored in the Mark Word, and the thread waiting for the lock also blocks.

For the vast majority of locks, there is no contention for the entire synchronization cycle. If there is no competition, lightweight locks use CAS to avoid the mutex overhead of heavyweight locks and improve program synchronization performance.

The relationship between state transformation of biased lock and lightweight lock and object Mark Word is as follows:

The resources

  • In-depth Understanding of the Java Virtual Machine
  • CyC2018: CS-Notes/Java concurrency
  • Hollis: An in-depth understanding of Multithreading (III) – Java object headers