Let's talk about the synchronized keyword

Posted by mandukar on Sun, 13 Feb 2022 09:43:53 +0100

Basic use of synchronized

Object lock

Custom object lock

/**
 * Object lock example 2
 */
public class SyncObjLock2 implements Runnable{
    private static SyncObjLock2 instance=new SyncObjLock2();

    private Object lock1=new Object();
    private Object lock2=new Object();

    /**
     * You can see that using sync will cause threads to queue up to take locks and serial together
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"coming");

        synchronized (lock1){
            System.out.println("Got it lock1 Lock, current thread name: "+Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        synchronized (lock2){
            System.out.println("Got it lock2 Lock, current thread name: "+Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(" "+Thread.currentThread().getName()+"be gone");
    }

    public static void main(String[] args) {

        Thread thread1=new Thread(instance);
        Thread thread2=new Thread(instance);
        thread1.start();
        thread2.start();
    }
}

this lock

/**
 * Object lock example
 */
public class SyncObjLock implements Runnable{
    private static SyncObjLock instance=new SyncObjLock();

    /**
     * You can see that using sync will cause the thread to serialize
     */
    @Override
    public void run() {
        System.out.println(" "+Thread.currentThread().getName()+"coming");

        synchronized (this){
            System.out.println("Get the object lock and the current thread name: "+Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(" "+Thread.currentThread().getName()+"be gone");
    }

    public static void main(String[] args) {

        Thread thread1=new Thread(instance);
        Thread thread2=new Thread(instance);
        thread1.start();
        thread2.start();
    }
}

Method lock

Common method lock

/**
 * Object lock example 3
 */
public class SyncObjLock3 implements Runnable {
    private static SyncObjLock3 instance = new SyncObjLock3();


    /**
     * You can see that using sync will cause threads to queue up to take locks and serial together
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "coming");

        lockMethod();


        System.out.println(" " + Thread.currentThread().getName() + "be gone");
    }

    public synchronized void lockMethod() {

        System.out.println("Got lock, current thread name: " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {

        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);
        thread1.start();
        thread2.start();
    }
}

Static method lock

/**
 * Object lock example 3
 */
public class SyncObjLockStaticMethod implements Runnable {
    private static SyncObjLockStaticMethod instance = new SyncObjLockStaticMethod();
    private static SyncObjLockStaticMethod instance2 = new SyncObjLockStaticMethod();


    /**
     * You can see that using sync will cause threads to queue up to take locks and serial together
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "coming");

        lockMethod();


        System.out.println(" " + Thread.currentThread().getName() + "be gone");
    }

    public static synchronized void lockMethod() {

        System.out.println("Got lock, current thread name: " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {

        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
    }
}

Class lock

/**
 * Object lock example 3
 */
public class SyncObjLockClassMethod implements Runnable {
    private static SyncObjLockClassMethod instance = new SyncObjLockClassMethod();
    private static SyncObjLockClassMethod instance2 = new SyncObjLockClassMethod();


    /**
     * You can see that using sync will cause threads to queue up to take locks and serial together
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "coming");

        lockMethod();


        System.out.println(" " + Thread.currentThread().getName() + "be gone");
    }

    public static void lockMethod() {

        synchronized (SyncObjLockClassMethod.class) {
            System.out.println("Got lock, current thread name: " + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }

    public static void main(String[] args) {

        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
    }
}

Precautions for the use of Synchronized

  1. A lock can only be acquired by one object at a time
  2. If you use static or Class decorated locks. All locks held by objects are current class objects
  3. When an exception is thrown, the sync lock will still release the lock

synchronized lock principle

Working principle of locking and unlocking

As shown below, we have such a sample code, a very simple example of sync lock

public class SynchronizedDemo2 {

    Object object = new Object();
    public void method1() {
        synchronized (object) {

        }
        method2();
    }

    private static void method2() {

    }
}


We first use javac to compile it into bytecode files

javac SynchronizedDemo2.java

Then use the javap command to decompile it

javap -verbose SynchronizedDemo2.class

It can be seen that the decompiled instructions include two instructions: monitorenter and monitorexit, which are the core instructions for locking and unlocking the sync lock

When a thread is about to execute the monitorenter instruction, it will add its own lock counter + 1, and then associate it with the monitor, which will always allow only one thread to obtain it per unit time. Therefore, when other threads try to associate with monitor, they cannot obtain the lock and can only wait.
When the thread obtaining the lock executes monitorexit, it will reset the lock counter to - 1. If it is not reduced to 0, it means that the lock has been re entered several times, and it can be re entered at will later. If you run out of 0, you can give it to others.

Refer to the following figure for the specific working principle

How reentrant works

As mentioned above, each lock object has a lock counter. When a thread monitorenter obtains the lock, the counter will be + 1. Next time it comes in and + 1, monitorexit will be - 1

Principle of ensuring visibility (pay attention to quoting your previous articles)

This involves a happends before principle, that is, unlocking a monitor and locking a monitor. As shown in the figure below, thread a releases the lock after thread b adds the lock according to the happends before principle, which ensures that the modification of thread a and thread b are visible.
Readers can refer to the author's article on the "happends before" principle
Concurrent programming must know and be able -- happens before

jvm optimization of locks

In jdk1 Before 6, sync was implemented by calling Mutex Lock at the bottom of the operating system. When Mutex Lock is called, it will suspend the thread and switch from user state to kernel state for execution. This kind of operation is very expensive.
Moreover, most of our scenarios are run in a single threaded environment (lock free competition environment). So jdk1 For example, lock coarsening, lock elimination, adaptive spin lock, bias lock and lightweight lock are used to reduce the performance overhead.

Introduction to lock types

Lock coarsening

For a large number of lock and unlock operations in a code segment, the jvm will automatically expand it into one lock and unlock

Lock elimination

The jlt compiler escape technology is used to judge whether the current synchronization code block data is also obtained by other synchronization code blocks. If not, the lock will be eliminated.

Lightweight Locking

This concept comes from a bold assumption. In most cases, threads run in a lock free competition environment. In this case, it is very unnecessary to call the Mutex Lock of the operating system. Therefore, we might as well use cas operation instead of monitorenter and monitorexit operation to try to take and release locks, so the overhead will be relatively less. When other threads fail to try to get the lock from cas, we call the Mutex Lock of the operating system and wake it up when the lock is released. Specifically, the author will detail it later. Here is a general concept.

Adaptive spin lock

As we know above, lightweight locks attempt to take and unlock locks through cas. Adaptive locks are born to solve the problem. cas fails to take the lock.
When the thread cas fails to get the lock, it will try again after a busy wait before the current thread calls the heavyweight lock associated with monitor. If it still fails, it will call the semaphore associated with monitor to enter the blocking state

Bias lock

In the lock free competitive environment, the reason to avoid unnecessary cas atomic execution is also very simple, although the overhead of cas lock is relatively small compared with Mutex Lock. However, there will still be some local delay.

Lifting stage of lock

According to the above, we probably know that the upgrade process of the lock is

	No lock->Bias lock->Lightweight Locking ->Heavyweight lock

And this process is irreversible

Lock optimization technology is introduced in detail

Spin lock and adaptive spin lock

First, let's talk about spin lock. From the above, we know that in order to reduce the performance overhead, we will use spin lock to reduce the locking overhead. However, the disadvantage of spin lock is also obvious. It will not count spin in the cpu. If it occupies cpu resources for a long time, it will affect the performance. Therefore, jdk sets that the thread will be suspended after a certain time is selected.
The default optional number of jdk is 10. Users can use the - XX:PreBlockSpin command to modify it.
But the embarrassing thing comes again. What if you don't get the lock during the spin and the lock is just released during the suspension of this thread? jdk considers this situation, so adaptive spin lock is born. If a thread gets the lock through self selection and is running during operation, the jvm will think that the probability of getting the lock is very high, and the number of spins will be increased. Conversely, you may omit the spin step and hang the lock directly.

Bias lock

Although cas lightweight locks solve the overhead of using heavyweight locks, we forget to consider the scenario of a thread repeatedly acquiring locks. This operation will also bring a lot of unnecessary performance overhead and context switching.
Therefore, HotSpot also optimizes this, but when a thread obtains the lock, it will find the stack frame in the object header of the lock object and store its lock record in the biased thread id. if the thread accesses again, we only need to check whether the local world pointed to in the markword of the lock is the same thread id to decide whether to allow the lock.

The release of biased lock is also very special. It will be released only when there is thread competition. First, it will pause the thread pointed to by the biased lock, and then judge whether it is alive or not. If it is dead, directly set the object header of the lock object to the unlocked state. On the contrary, either point the bias lock to the thread that wants to hold the lock, or set it to no lock, or the tag object cannot be used as a bias lock.

Lock coarsening

Since the concept of lock coarsening has been introduced above, it will be analyzed in the form of code. In fact, the concept is very simple. The following code uses StringBuffer to ensure thread safety, which makes the append operation lock and unlock continuously, which will have a great impact on the performance. The jvm will optimize this series of append operations into the previous lock, Implement a series of append operations in this lock, which is lock coarsening

/**
     * Lock coarsening
     * @param s1
     * @param s2
     * @param s3
     * @return
     */
    public static String test04(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }

Lock elimination

The concept of lock elimination has been described above. Let's take a practical example here. As shown in the following code, we all know that the bottom layer of String splicing operation is realized by creating a new String object. Therefore, we may see the instructions of StringBuffer or StringBuilder by compiling and decompiling.

 public static String test03(String s1, String s2, String s3) {
        String s = s1 + s2 + s3;
        return s;
    }

The above code is in a lock free competitive environment, so the smart jvm will optimize it after learning about it through escape technology, and use StringBuilder to realize string splicing, as shown in the following figure

Lightweight Locking

The concept of lightweight lock is also very simple, as shown in the figure below. When a thread wants to execute synchronous code blocks, it will create a space of * * lock record * * in its own stack frame

Then try to acquire the lock Object through cas. If the acquisition is successful, it will be marked as 01, and then the MarkWord of the lock Object will be updated to 00, which means that the Object is currently in the state of acquiring lightweight lock. Then point the pointer of the stack frame of the Object to the address of the thread Lock Record

If it is not obtained, the jvm will judge whether the pointer of the stack frame of the Object lock points to the Lock Record of the current lock taking thread. If so, it indicates that the lock has been taken successfully and let it continue to do what it should do. On the contrary, mark the lock as 10, that is, expand it into a heavyweight lock and wait for the thread holding the lock to release.

Lock comparison

Comparison between synchronized and lock

Disadvantages of synchronized

  1. The efficiency is low. Only when the lock is exhausted or abnormal will it be released. Moreover, the lock timeout cannot be set, and the thread interrupts the use of the lock.
  2. The use of a single lock has a single condition. Each lock has only one single condition. It is not as good as a read-write lock.
  3. I can't tell if I succeeded in getting the lock

Synchronized locks are only associated with one Condition (whether to acquire locks) and are not flexible. Later, the combination of Condition and Lock solved this problem. When multiple threads compete for a Lock, the other threads that do not get the Lock can only keep trying to get the Lock without interruption. High concurrency can lead to performance degradation. The lockinterrupt () method of ReentrantLock can give priority to responding to interrupts. If a thread waits too long, it can interrupt itself, and then ReentrantLock responds to the interrupt and does not let the thread continue to wait. With this mechanism, when using ReentrantLock, there will be no deadlock like synchronized.

Deeper understanding of synchronized

synchronized is implemented through the jvm and is easy to use. Of course, the following points should be paid attention to when using:

  1. The function of the lock should not be too large
  2. Lock object cannot be empty
  3. Don't use synchronized or lock locks if you don't have to. Try to use various classes in the juc package as much as possible
  4. Avoid deadlock

Is synchronized a fair lock

No, the new thread can get the monitor immediately, which is likely to cause the waiting thread to wait continuously. Therefore, improper use is likely to lead to thread starvation

reference

Key words: synchronized detailed explanation

Source address

Add link description

Topics: Java Spring Interview Concurrent Programming