Java concurrent programming: Synchronized and its realization principle

1. Basic use of Synchronized

Synchronized is one of the most commonly used methods to solve concurrency problems in Java, and it is also the simplest method. Synchronized has three main functions: (1) to ensure that threads are mutually exclusive to access synchronization code (2) to ensure that the modification of shared variables can be seen in time (3) to effectively solve the reordering problem. Syntactically speaking, there are three usages of Synchronized:


1. Modification of ordinary methods
2. Modification of static methods
3. Modification of code blocks

Next, I will use a few example programs to illustrate these three ways of use (for the sake of comparison, the three pieces of code are basically the same except for the different ways of using Synchronized).

1. When there is no synchronization:

Code snippet one:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

The execution result is as follows. Thread 1 and Thread 2 enter the execution state at the same time. Thread 2 executes faster than Thread 1, so thread 2 is executed first. Thread 1 and Thread 2 are executed simultaneously in this process.

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end

2. Synchronize with common methods:

Code snippet two:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public synchronized void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public synchronized void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

The execution result is as follows. Compared with the code segment, it is obvious that thread 2 needs to wait for the execution of method 1 of thread 1 to finish before starting to execute method 2 method.

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

3. Synchronization of static methods (classes)

Code snippet three:

package com.paddx.test.concurrent;
 
 public class SynchronizedTest {
     public static synchronized void method1(){
         System.out.println("Method 1 start");
         try {
             System.out.println("Method 1 execute");
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 1 end");
     }
 
     public static synchronized void method2(){
         System.out.println("Method 2 start");
         try {
             System.out.println("Method 2 execute");
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 2 end");
     }
 
     public static void main(String[] args) {
         final SynchronizedTest test = new SynchronizedTest();
         final SynchronizedTest test2 = new SynchronizedTest();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method1();
             }
         }).start();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 test2.method2();
             }
         }).start();
     }
 }

The execution results are as follows. The synchronization of static methods is essentially the synchronization of classes (static methods are essentially methods belonging to classes, not methods on objects), so even if test and test2 belong to different objects, they both belong to SynchronizedTest Instances of classes, so method1 and method2 can only be executed sequentially, not concurrently.

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

4. Code block synchronization

Code snippet four:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            synchronized (this) {
                System.out.println("Method 1 execute");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            synchronized (this) {
                System.out.println("Method 2 execute");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

The execution result is as follows. Although both thread 1 and thread 2 enter the corresponding method to start execution, thread 2 needs to wait for the execution of the synchronization block in thread 1 to complete before entering the synchronization block.

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end

2. Synchronized principle

If you have any questions about the above execution results, don't worry, let's first understand the principle of Synchronized, and then we will see the above problems at a glance. Let's first look at how Synchronized synchronizes code blocks by decompiling the following code:

package com.paddx.test.concurrent;

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

Decompilation result:

Regarding the role of these two instructions, we directly refer to the description in the JVM specification:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

The general meaning of this passage is:

Each object has a monitor lock (monitor). When the monitor is occupied, it will be locked. When the thread executes the monitorenter instruction, it tries to acquire the ownership of the monitor. The process is as follows:

If the number of entries of the monitor is 0, the thread enters the monitor, and then the number of entries is set to 1, and the thread is the owner of the monitor.

If the thread has already occupied the monitor and just re-entered, the number of entries entering the monitor is increased by 1.

If other threads have already occupied the monitor, the thread enters the blocking state until the number of entries in the monitor is 0, and then retry to acquire the ownership of the monitor.

monitorexit: 

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

The general meaning of this passage is:

The thread executing monitorexit must be the owner of the monitor corresponding to objectref.

When the instruction is executed, the entry number of the monitor is decreased by 1. If the entry number is 0 after subtracting 1, the thread exits the monitor and is no longer the owner of the monitor. Other threads blocked by this monitor can try to acquire ownership of this monitor. 

Through these two paragraphs of description, we should be able to clearly see the implementation principle of Synchronized. The semantic bottom of Synchronized is completed by a monitor object. In fact, wait/notify and other methods also depend on the monitor object. This is why only in synchronization Wait/notify and other methods can be called in the block or method, otherwise it will throw java.lang.IllegalMonitorStateException.

Let's look at the decompilation result of the synchronization method:

Source code:

package com.paddx.test.concurrent;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

Decompilation result:

From the results of decompilation, the synchronization of the method is not completed by the instructions monitorenter and monitorexit (theoretically, it can also be achieved by these two instructions), but compared to the ordinary method, the constant pool has an additional ACC_SYNCHRONIZED identifier . The JVM implements method synchronization based on this identifier: when the method is called, the calling instruction will check whether the ACC_SYNCHRONIZED access flag of the method is set. If it is set, the execution thread will first acquire the monitor, and the method body can be executed after the acquisition is successful. , And release the monitor after the method is executed. During the execution of the method, no other thread can obtain the same monitor object. In fact, there is no difference in essence, but method synchronization is realized in an implicit way, without bytecode.

Three, operation result interpretation

With an understanding of the principle of Synchronized, it can be solved by looking at the above program again.

1. Results of code segment 2:

Although method1 and method2 are different methods, these two methods are synchronized and are called through the same object. Therefore, before calling, you need to compete for the lock (monitor) on the same object. Locks can be acquired mutually exclusive, therefore, method1 and method2 can only be executed sequentially.

2. Results of code segment 3:

Although test and test2 belong to different objects, test and test2 belong to different instances of the same class. Because method1 and method2 are static synchronization methods, you need to get the monitor on the same class when calling (each class corresponds to only one class object ), so it can only be executed sequentially.

3. Results of code segment 4:

The synchronization of the code block essentially needs to obtain the monitor of the object in the brackets after the Synchronized keyword. Since the content of the brackets in this code is this, and method1 and method2 are called through the same object, so before entering the synchronization block Need to compete for locks on the same object, so only synchronized blocks can be executed sequentially.

Four, summary

Synchronized is the most commonly used way to ensure thread safety in Java concurrent programming, and its use is relatively simple. But if we can understand its principles in depth and understand the underlying knowledge of monitor locks, on the one hand, it can help us use the Synchronized keyword correctly, and on the other hand, it can also help us better understand the concurrent programming mechanism and help us in Choose a more optimal concurrency strategy to complete the task in different situations. It is also able to calmly deal with various concurrency problems encountered in ordinary times.

Guess you like

Origin blog.csdn.net/bj_chengrong/article/details/96830136