The problem

(1) Synchronized?

(2) The realization principle of synchronized?

(3) Whether synchronized is reentrant?

(4) Is synchronized a fair lock?

(5) Optimization of synchronized?

(6) Five ways of using synchronized?

Introduction to the

Synchronized keyword is the most basic synchronization method in Java. When compiled, monitorenter and Monitorexit bytecode instructions are generated before and after the synchronized block. Both of these bytecode instructions require a reference type parameter to specify the object to be locked and unlocked.

Realize the principle of

When we looked at the Java memory model, we introduced two instructions: Lock and unlock.

Lock a variable that acts on main memory and identifies a variable in main memory as a thread-exclusive state.

Unlock, unlock, a variable that operates on main memory, it frees a locked variable so that it can be locked by another thread.

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

Synchronized is implemented using monitorenter and Monitorexit directives.

According to the JVM specification, when monitorenter executes, it first attempts to acquire the lock on an object. If the object is not locked or the current thread already owns the lock, it increments the lock counter. Accordingly, monitorexit decrement the counter by one. When the counter decreases to 0, the lock is released.

Let’s look at the compiled bytecode and see what it looks like:

public class SynchronizedTest {

    public static void sync(a) {
        synchronized (SynchronizedTest.class) {
            synchronized (SynchronizedTest.class) {
            }
        }
    }

    public static void main(String[] args) {}}Copy the code

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

The bytecode instructions for the compiled sync() method are as follows, annotated for ease of reading:

/ / load in the constant pool SynchronizedTest class object to the LDC operand stack of 0 # 2 < com/coolcoding/code/the synchronize/SynchronizedTest > element 2 dup / / / / copy stack Store a reference to the local variable 0, which is the number of variables 3 astore_0 SynchronizedTest > monitorenter // reload SynchronizedTest from the constant pool into operand stack 5 LDC #2 < com/coolcoding/code/the synchronize/SynchronizedTest > element 7 dup / / / / copy stack to store a reference to a local variable 1 in 8 astore_1 / / call monitorenter again, Its argument is variable 1, which is also a SynchronizedTest object 9 Monitorenter // loads the first variable from the local variable table 10 ALOad_1 // calls Monitorexit to unlock Its arguments are the variable loaded above 1 11 MONIToreXIT // Skip to line 20 12 goto 20 (+8) 15 ASTore_2 16 ALOAD_1 17 MonitoreXIT 18 ALOad_2 19 athrow // Load the 0 variable 20 aload_0 from the local variable table // call monitorexit to unlock, Its arguments are the variable loaded above 0 21 MONIToreXIT // Skip to line 30 22 goto 30 (+8) 25 ASTore_3 26 ALOAD_0 27 MonitoreXIT 28 ALOad_3 29 athrow // Method returns, end 30 returnCopy the code

Synchronized locks SynchronizedTest and loads SynchronizedTest twice from the pool into 0 and 1. Synchronized locks SynchronizedTest and loads SynchronizedTest twice from the pool into 0 and 1. Synchronized locks SynchronizedTest and loads SynchronizedTest into 0 and 1. In the reverse sequence, variable 1 is unlocked first, and then 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, It is not detailed here. If you are interested, you can read the book “The Art of Java Concurrent Programming”.

A PDF version of the book is available by replying to “JMM”.

Atomicity, visibility, order

When we talked about the Java memory model, we said that the memory model is mainly used to solve the problem of cache consistency, which includes atomicity, visibility, and ordering.

Does the synchronized keyword guarantee these three properties?

Returning to the Java memory model, the synchronized keyword base is implemented through Monitorenter and Monitorexit, which in turn are implemented through lock and unlock.

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

(1) A variable can only be locked by one thread at a time, but the lock operation can be performed by the same thread several times. After the lock operation is performed several times, the variable can be unlocked only after the same number of UNLOCK operations are performed.

(2) If the lock operation is performed on a variable, the value of the variable will be emptied from the working memory. Before the variable can be used by the execution engine, the load or assign operation needs to be performed again to initialize the value of the variable.

(3) If a variable is not locked by a lock operation, it is not allowed to unlock it, nor is it allowed to unlock a variable that is locked by another thread.

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

From rule (1), we know that synchronized is atomic because only one thread is allowed access to code between lock and unlock.

From rules (1), (2) and (4), we know that each time a lock and unlock load a variable from main memory or flush it back to main memory. A variable between a lock and unlock cannot be modified by another thread, so synchronized is visible.

According to rules (1) and (3), we know that all locks on variables must be queued, and other threads are not allowed to unlock the object locked by the current thread. Therefore, synchronized is orderly.

In summary, synchronized can guarantee atomicity, visibility and order.

Fair locks VS unfair locks

From the above, we know the implementation principle of synchronized, and it is reentrant. Is it fair lock?

Serve straight:

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(Threads "4")).start(); }}Copy the code

In this program, we started four threads with an interval of 100ms, and each thread printed a sentence and waited 1000ms. If synchronized is a fair lock, the printed results should be threads 1, 2, 3 and 4 in sequence.

Synchronized is an unfair lock, however, because the actual results rarely look like the above.

Lock the optimization

As Java evolves, so do older things like synchronized in Java, such as ConcurrentHashMap, which used ReentrantLock in JDK7, Synchronized has been changed to native synchronized in JDK8, so it can be seen that synchronized has native support and still has a lot of room to evolve.

So what are the evolving states of synchronized?

Here is a brief introduction:

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

(2) Lightweight lock means that when the lock is biased and accessed by another thread, the biased lock will be upgraded to lightweight lock. This thread will try to obtain the lock by means of spin without blocking and improve performance.

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

conclusion

(1) Synchronized generates monitorenter and Monitorexit bytecode instructions before and after synchronized blocks during compilation;

(2) Monitorenter and Monitorexit bytecode directives require a reference type, not a base type;

(3) The monitorenter and Monitorexit bytecode instructions are lower down, using Java memory model lock and unlock instructions;

(4) synchronized is reentrant lock;

(5) synchronized is unfair lock;

(6) Synchronized can simultaneously ensure atomicity, visibility and order;

(7) Synchronized has three states: biased lock, lightweight lock, heavyweight lock;

Eggs — Five ways to use synchronized

Based on the above analysis, we know that synchronized requires a reference type parameter, which can be divided into three categories in Java: class object, instance object and common reference. The usage methods are as follows:

public class SynchronizedTest2 {

    public static final Object lock = new Object();

    Synchronizedtest.class locks the synchronizedTest.class object
    public static synchronized void sync1(a) {}public static void sync2(a) {
        Synchronizedtest.class locks the synchronizedTest.class object
        synchronized (SynchronizedTest.class) {

        }
    }

    // Lock the current instance this
    public synchronized void sync3(a) {}public void sync4(a) {
        // Lock the current instance this
        synchronized (this) {}}public void sync5(a) {
        // Lock the specified object lock
        synchronized (lock) {

        }
    }
}
Copy the code

When using synchronized in methods, pay attention to the implicit parameter transmission, which can be divided into static methods and non-static methods. The implicit parameter of the static method is the current class object, and the implicit parameter of the non-static method is the current instance this.

In addition, multiple synchronized codes are synchronized only if they lock the same object. This must be noted when using synchronized.

Recommended reading

  1. JMM (Java Memory Model)

  2. Volatile parsing of the Java Synchronization series


Welcome to pay attention to my public number “Tong Elder brother read source code”, view more source code series articles, with Tong elder brother tour the ocean of source code.