1. Introduction of synchronized

In my last post, I analyzed the atomicity problem in the JAVA memory model and the volatile keyword with the following code:

    public static volatile int num = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run(a) {
                    for (int i = 0; i < 1000; i++) { num++; }}}); thread.start(); } Thread.sleep(1000);
        System.out.println(num);
    }
Copy the code

Using volatile to modify num (to shield from visibility problems) would have been 20000 if the calculation had worked, but the result was not 20000 many times, and each time the result was inconsistent.

So why does this happen?Copy the code

In the case of concurrent execution by multiple threads, multiple threads loaded num into the register for calculation before volatile could lock, and one thread was locked successfully, invalidating the NUM in the CPU cache. However, the num that had been loaded into the register would not be invalidating, and would still be calculated and written back to memory. This caused the final value of num to be different from what we expected!

So how do you solve this data security problem?Copy the code

Since there are data security issues in multithreading, the shared resources with data security issues are only allowed to be accessed by one thread at a time, thus avoiding such problems. JAVA provides the synchronized keyword, which provides single-thread access to shared resources.

2. Synchronized?

2.1 Modify static methods

    public static volatile int num = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
                getNumStatic();
            });
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println(num);
    }
    // Decorate static methods
    public synchronized static int getNumStatic(a){
        for (int i1 = 0; i1 < 1000; i1++) {
            num++;
        }
        return num;
    }
Copy the code

When synchronized modifies static methods, the locked object is the current class class, so any thread that accesses the method is competing for the same lock. This means that if there are two business-unrelated synchronized modified methods in the class, other threads will not be able to access the other method.

2.2 Modify instance methods

    public static volatile int num = 0;
    public static void main(String[] args) throws InterruptedException {
        java obj = new java();
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
                obj.getNumStatic();
            });
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println(num);
    }

    public synchronized int getNumStatic(a){
        for (int i1 = 0; i1 < 1000; i1++) {
            num++;
        }
        return num;
    }
Copy the code

When synchronized modifies instance methods, it locks the obJ object that is currently new. When different threads have already accessed the same object instance, they compete for the same lock.

Written as follows causes synchronized to fail

    public static volatile int num = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
                // Different threads compete for different lock objects, which invalidates the contention.
                java obj = new java();
                obj.getNumStatic();
            });
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println(num);
    }

    public synchronized int getNumStatic(a){
        for (int i1 = 0; i1 < 1000; i1++) {
            num++;
        }
        return num;
    }
Copy the code

2.3 Embellish code blocks

    public static Object object = new Object();

    public int getNumStatic(a){
        for (int i1 = 0; i1 < 1000; i1++) {
            synchronized(object){ num++; }}return num;
    }
Copy the code

When synchronized modifies code blocks, the locked object is the object behind synchronized. Ensure the global uniqueness of object; otherwise, it will fail.

2.3.1 Variants – Modify this

    public int getNumStatic(a){
        for (int i1 = 0; i1 < 1000; i1++) {
            synchronized (this){ num++; }}return num;
    }
Copy the code

When synchronized modifies this, the lock object is the instance object that calls the method. Different threads must access the same instance object; otherwise, the same object fails

3. Principle of synchronized locking

Synchronized is implemented based on JVM built-in locks by entering and exiting Monitor objects. Synchronized is a heavyweight lock that was optimized after JDK1.5.

3.1 Monitor lock -Monitor

The monitor lock is based on the underlying operating system Mutex lock (Mutex lock) implementation, which involves the transition from user state to kernel state to user state, so it is a heavyweight lock.

Each object is associated with a Monitor object, and when the Monitor object is held, that object is locked. Monitor lock -Monitor is implemented in the JVM by entering/exiting the Monior object. While different JVM implementations differ, all can be entered/exited through the MonitorEnter/MonitorExit directives.

  • MonitorEnter: Each object is a Monitor lock. The monitor is locked when it is occupied, and the thread attempts to take ownership of the Monitor when it executes the Monitorenter instruction
    • When the entry number of the Monior is 0, the thread enters the Monior object and sets the entry number to 1. The thread is the holder of the object
    • If the thread already holds the object, the number of entries is +1 upon re-entry.
    • If another thread already holds the object, the wait is blocked.
  • MonitorExit: Only the thread of this object can execute MonitorExit. After MonitorExit is executed, the number of entries is -1 until the number of entries reaches zero, and the lock is released.

The Monitor object exists in the object header Mark Word (the pointer to the stored pointer) of each Java object, i.eThe lock state is recorded in the object header of each object(Mark Word).

3.2 Java Object Headers

In the HotSpot VIRTUAL machine, objects are stored in memory in three main parts: the object header, the instance data, and the padding area for it.

  • Object header type
    • Common objects include: Mark Word (runtime class information), Klass Pointer (class object Pointer to method area)
    • Array objects include: Mark Word; Klass Pointer; Array Length

The lock status is recorded in the Mark Word (runtime class information), as shown

4. Synchronized lock expansion upgrade

Synchronized was a heavyweight lock prior to JDK1.5, and all synchronized code is directly locked by MonitorEnter/MonitorExit operations on monior objects. This leads to “user and kernel” switching back and forth, which is inefficient.

After jdk1.5, Oracle optimized synchronized, Lock Coarsening, Lock Elimination, Lightweight Locking, Biased Locking and Adaptive Locking are added Techniques such as Spinning) to reduce the overhead of locking operations.

General process: no lock state → biased lock → lightweight lock → heavyweight lock.

The detailed process is shown as follows:

4.1 lock coarsening

When the JVM senses that a series of consecutive operations are locking the same object, and there is no competition from other threads, the JVM increases the scope of the lock (from the first operation to the last operation) because locking and unlocking the same object frequently is costly.

    public int getNumStatic(a){
        for (int i1 = 0; i1 < 1000; i1++) {
            // Loop to lock the same object
            synchronized(object){ num++; }}return num;
    }
Copy the code

Will be optimized to look like code

    public int getNumStatic(a){
        synchronized (object){
            for (int i1 = 0; i1 < 1000; i1++) { num++; }}return num;
    }
Copy the code

4.2 lock elimination

When the JVM determines through escape analysis that a block of code is not locked, the JVM phase removes the lock.

4.3 biased locking

In most cases, there is no multi-thread contention for locks, and locks are always acquired by the same thread multiple times. Therefore, biased locks are introduced to reduce the cost of acquiring locks by the same thread (some CAS operations are involved in lock switching, which is time-consuming).

Biased locking the core idea is that if a thread got a lock and then lock into bias mode, the structure of Mark Word become biased locking structure, when the thread lock request again, no need to do any synchronization operation, namely the process of acquiring a lock, so it saves a lot of unnecessary lock application operation, so as to optimize the application performance.

In the case of fierce multithreading competition, enabling biased locking means that the biased thread of the lock must be modified by CAS before each lock is added, which is not worth the loss.

The default open open biased locking: – XX: XX: + UseBiasedLocking BiasedLockingStartupDelay = 0, close to lock: – XX: – UseBiasedLocking

4.4 Lightweight Lock

In cases where biased locking fails, the JVM upgrades to lightweight locking. Lightweight locking says: Most of the time, there is no lock contention during a thread’s execution. (Thread alternate execution)

Lightweight locks will also fail if a large number of threads compete for the same lock.

Suppose there are 1000 locks competing for the same lock at the same time, and the average thread execution time is 1ms.

  • There is no need to upgrade to a heavyweight lock at this point.
  • It is also time consuming to suspend other threads and then wake them up when needed.

So the JVM introduced spin locking.

4.5 the spin lock

After the lightweight lock fails, the VIRTUAL machine prevents the thread from actually hanging at the operating system level by making the waiting thread go through several empty loops in the hope that the lightweight lock will be released and taken over by the thread in the loop. The number of cycles is specified. If the lock cannot be obtained within the cycle, the lock is upgraded to a heavyweight lock.