Detailed interpretation of synchronized analysis of java synchronization

  • 2021-07-26 07:54:55
  • OfStack

Problem

(1) What are the characteristics of synchronized?

(2) How does synchronized work?

(3) Is synchronized reentrant?

(4) Is synchronized a fair lock?

(5) Optimization of synchronized?

(6) Five ways to use synchronized?

Brief introduction

synchronized keyword is the most basic synchronization means in Java. After it is compiled, monitorenter and monitorexit bytecode instructions will be generated before and after the synchronization block respectively. Both bytecode instructions need a parameter of reference type to indicate the object to be locked and unlocked.

Implementation principle

When studying the Java memory model, we introduced two instructions: lock and unlock.

lock, lock, acts on variables in main memory, which identifies variables in main memory as one thread exclusive state.

unlock, Unlock, acts on the main memory variables, it releases the locked variables, released variables can be locked by other threads.

However, these two instructions are not provided directly to the user. Instead, two higher-level instructions, monitorenter and monitorexit, are provided to implicitly use the lock and unlock instructions.

synchronized is implemented using monitorenter and monitorexit instructions.

According to the requirements of JVM specification, when executing monitorenter instruction, we must first try to acquire the lock of the object. If the object is not locked, or the current thread already owns the lock of the object, we will increase the lock counter by 1. Accordingly, when executing monitorexit, we will decrease the counter by 1. When the counter is reduced to 0, the lock will be released.

Let's take a piece of code and see what the compiled bytecode looks like to learn:


public class SynchronizedTest {

 public static void sync() {
 synchronized (SynchronizedTest.class) {
 synchronized (SynchronizedTest.class) {
 }
 }
 }
 public static void main(String[] args) {
 }
}

Our code is simple, simply adding synchronized to the SynchronizedTest. class object twice, and nothing else.

The bytecode instruction of the compiled sync () method is as follows. In order to facilitate reading, Tong Ge specially added comments:


//  Loads the in the constant pool SynchronizedTest Class object to operand stack 
0 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest>
//  Copy top element of stack 
2 dup
//  Storage 1 References to local variables 0 Middle and back 0 Which variable is represented 
3 astore_0
//  Call monitorenter And its parameter variables 0 , that is, the above SynchronizedTest Class object 
4 monitorenter
//  Re-load the SynchronizedTest Class object to operand stack 
5 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest>
//  Copy top element of stack 
7 dup
//  Storage 1 References to local variables 1 Medium 
8 astore_1
//  Call again monitorenter Whose arguments are variables 1 , also still SynchronizedTest Class object 
9 monitorenter
//  Loads the first from the local variable table 1 Variable 
10 aload_1
//  Call monitorexit Unlock, whose parameters are the variables loaded above 1
11 monitorexit
//  Jump to number one 20 Row 
12 goto 20 (+8)
15 astore_2
16 aload_1
17 monitorexit
18 aload_2
19 athrow
//  Loads the first from the local variable table 0 Variable 
20 aload_0
//  Call monitorexit Unlock, whose parameters are the variables loaded above 0
21 monitorexit
//  Jump to number one 30 Row 
22 goto 30 (+8)
25 astore_3
26 aload_0
27 monitorexit
28 aload_3
29 athrow
//  Method returns, ending 
30 return

Read according to Tong Ge's notes, Bytecode is relatively simple. Our synchronized locks SynchronizedTest class objects. We can see that it loads SynchronizedTest class objects twice from the constant pool and stores them in local variable 0 and local variable 1 respectively. When unlocking, it is in the opposite order. First unlock variable 1, then unlock variable 0. In fact, variable 0 and variable 1 point to the same object, so synchronized is reentrant.

As for how the locked object is stored in the object header, Tong Ge will not elaborate here. If you are interested, you can look at the book "The Art of Java Concurrent Programming".

Atomicity, visibility, order

When explaining the Java memory model earlier, we said that the memory model is mainly used to solve the problem of cache uniformity, which mainly includes atomicity, visibility and order.

So, can the synchronized keyword guarantee these three features?

Back to the Java memory model, the underlying synchronized keyword is implemented through monitorenter and monitorexit, and these two instructions are implemented through lock and unlock.

lock and unlock must meet the following four rules in the Java memory model:

(1) Only one thread is allowed to perform lock operation on a variable at the same time, but lock operation can be performed by the same thread for many times. After lock is performed for many times, the variable can be unlocked only after unlock operation is performed for the same number of times.

(2) If lock operation is performed on a variable, the value of this variable in the working memory will be emptied. Before the execution engine uses this variable, it is necessary to re-execute load or assign operation to initialize the value of the variable;

(3) If a variable is not locked by lock operation, it is not allowed to perform unlock operation on it, and it is not allowed to lock variables by unlock1 other threads;

(4) Before performing unlock operation on a variable, the variable must be synchronized back to the main memory, that is, store and write operations must be performed;

From Rule (1), we know that only one thread is allowed to access the code between lock and unlock at the same time, so synchronized is atomic.

From rules (1), (2) and (4), we know that every time lock and unlock, variables are loaded from main memory or flushed back to main memory, while variables between lock and unlock (locked variables in this case) will not be modified by other threads, so synchronized is visible.

Through rules (1) and (3), we know that all locking of variables must be queued, and other threads are not allowed to unlock objects locked by the current thread, so synchronized is orderly.

To sum up, synchronized can guarantee atomicity, visibility and order.

Fair Lock VS Unfair Lock

Through the above study, we know the implementation principle of synchronized, and it is reentrant, so is it a fair lock?

Serve directly:


public class SynchronizedTest {
 public static void sync(String tips) {
 synchronized (SynchronizedTest.class) {
 System.out.println(tips);
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 }
 public static void main(String[] args) throws InterruptedException {
 new Thread(()->sync(" Thread 1")).start();
 Thread.sleep(100);
 new Thread(()->sync(" Thread 2")).start();
 Thread.sleep(100);
 new Thread(()->sync(" Thread 3")).start();
 Thread.sleep(100);
 new Thread(()->sync(" Thread 4")).start();
 }
}

In this program, we started four threads and started them at intervals of 100ms. After printing one sentence in each thread, we waited for 1000ms. If synchronized is a fair lock, the printed results should be threads 1, 2, 3 and 4 in turn.

However, the result of actual operation will hardly look like the above, so synchronized is an unfair lock.

Lock optimization

Java is constantly evolving. Similarly, ancient things like synchronized in Java are constantly evolving. For example, ConcurrentHashMap was locked with ReentrantLock when jdk7, and it was replaced with native synchronized when jdk8. It can be seen that synchronized has native support, and its evolution space is still very large.

So, what are the evolutionary states of synchronized?

Let's make a brief introduction here:

(1) Biased lock means that a piece of synchronization code 1 is directly accessed by a thread, so this thread will automatically acquire the lock, reducing the cost of acquiring the lock.

(2) Lightweight lock means that when the lock is biased lock, it is accessed by another thread, and the biased lock will be upgraded to lightweight lock, and this thread will try to acquire the lock by spinning, which will not block and improve performance.

(3) Heavyweight lock means that when the lock is a lightweight lock, when the spinning thread spins for a certain number of times and has not yet acquired the lock, it will enter a blocking state, and the lock will be upgraded to a heavyweight lock, which will block other threads and reduce their performance.

Summarize

(1) synchronized generates monitorenter and monitorexit bytecode instructions before and after the synchronization block at compile time;

(2) monitorenter and monitorexit bytecode instructions need a parameter of reference type, but the basic type is not allowed;

(3) The lower layer of monitorenter and monitorexit bytecode instructions are lock and unlock instructions using Java memory model;

(4) synchronized is a reentrant lock;

(5) synchronized is an unfair lock;

(6) synchronized can guarantee atomicity, visibility and order at the same time;

(7) synchronized has three states: bias lock, lightweight lock and heavyweight lock;

Eggs--Five Ways to Use synchronized

Through the above analysis, we know that synchronized needs a parameter of reference type, and the parameters of this reference type can actually be divided into three categories in Java: class objects, instance objects and common references, which are used as follows:


public class SynchronizedTest2 {
 public static final Object lock = new Object();
 //  Lock is SynchronizedTest.class Object 
 public static synchronized void sync1() {
 }
 public static void sync2() {
 //  Lock is SynchronizedTest.class Object 
 synchronized (SynchronizedTest.class) {
 }
 }
 //  Lock is the current instance this
 public synchronized void sync3() {
 }
 public void sync4() {
 //  Lock is the current instance this
 synchronized (this) {
 }
 }
 public void sync5() {
 //  Lock is the specified object lock
 synchronized (lock) {
 }
 }
}

When using synchronized in methods, it should be noted that parameters will be implicitly passed, which can be divided into static methods and non-static methods. The implicit parameters on static methods are the current class object, and the implicit parameters on non-static methods are the current instance this.

In addition, multiple synchronized only locks the same object, and the code between them is synchronized. This point should be paid attention to when using synchronized.

Recommended reading

JMM of java Synchronization (Java Memory Model)

volatile Analysis of java Synchronization


Related articles: