Java high concurrent programming 2--Synchronized and its realization principle

First, the basic use of Synchronized

Previous << Java High Concurrency Programming 1--What is a thread >>

Synchronized is one of the most common ways to solve concurrency problems in Java, and it is also the easiest way. There are three main functions of Synchronized: (1) To ensure that the access synchronization code of threads is mutually exclusive (2) To ensure that the modification of shared variables can be seen in time (3) To effectively solve the problem of reordering. Syntactically, there are three uses of Synchronized in total:

  1. The effect of modifying ordinary methods  is equivalent to locking the current instance. Before entering the synchronization code, the lock of the current instance must be obtained.
  2. The role of the modified static method  is equivalent to locking the current class object. Before entering the synchronization code, the lock of the current instance must be obtained.
  3. The modified code block  specifies the lock object, locks the given object, and enters the synchronization code block to obtain the lock of the given object

Next, I will illustrate these three usage methods through several example programs (for the convenience of comparison, the three pieces of code are basically the same except for the different usage methods of Synchronized).

1. Without synchronization:

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();
    }
}
/*执行结果如下,线程1和线程2同时进入执行状态,线程2执行速度比线程1快,
所以线程2先执行完成,这个过程中线程1和线程2是同时执行的。

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end*/

2. Synchronize common methods:

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();
    }
}
/*执行结果如下,跟代码段一比较,可以很明显的看出,线程2需要等待线程1的method1执行完成才能开始执行method2方法。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end*/

3. Static method (class) synchronization

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();
     }
 }
/*执行结果如下,对静态方法的同步本质上是对类的同步
(静态方法本质上是属于类的方法,而不是对象上的方法),
所以即使test和test2属于不同的对象,但是它们都属于SynchronizedTest类的实例,
所以也只能顺序的执行method1和method2,不能并发执行。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end*/

4. Code block synchronization

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();
    }
}
/*执行结果如下,虽然线程1和线程2都进入了对应的方法开始执行,但是线程2在进入同步块之前,
需要等待线程1中同步块执行完成。

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end*/

2. Synchronized principle

If you still have doubts about the above execution results, don't worry, let's first understand the principle of Synchronized, and then go back to the above problems to be clear 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");
        }
    }
}

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.

This sentence roughly means:

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

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

2. If the thread already owns the monitor and just re-enters it, the number of entries into the monitor is incremented by 1.

3. If other threads have already occupied the monitor, the thread enters the blocking state until the monitor's entry count is 0, and then retry to obtain 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.

This sentence roughly means:

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 decremented by 1. If the entry number is 0 after decrementing 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 descriptions, we should be able to clearly see the implementation principle of Synchronized. The underlying semantics of Synchronized is completed through a monitor object. In fact, methods such as wait/notify also depend on the monitor object, which is why only in synchronization Wait/notify and other methods can only be called in the block or method of the .

  Let's take a look at the decompilation results of the synchronized method:

package com.paddx.test.concurrent;

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

 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 with the ordinary method, the ACC_SYNCHRONIZED identifier is added in the constant pool. . 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 obtain the monitor, and then execute the method body after the acquisition is successful. , the monitor is released after the method is executed. During the execution of the method, no other thread can obtain the same monitor object again. In fact, there is no difference in essence, but the synchronization of the method is implemented in an implicit way, without the need for bytecode.

3. Interpretation of operating results

With the understanding of the principle of Synchronized, and then look at the above program can be easily solved.

1. Code segment 2 results:

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

2. Code segment 3 results:

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

3. Code segment 4 results:

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

Four synchronized reentrancy

You can see the example project <<code cloud git _oo9>>

1. Reentrant

A program or subprogram can be "reentrant or re-entrant if it can be "interrupted at any time and then the operating system schedules another piece of code that calls the subprogram without error". of. That is, while the subroutine is running, the thread of execution can re-enter and execute it, and still get the results expected at design time. Unlike the thread safety of multi-threaded concurrent execution, reentrancy emphasizes that it is still safe to re-enter the same subroutine when executed on a single thread.

2. Reentrant Conditions

  • Do not use static or global data within functions.
  • No static or global data is returned, all data is provided by the caller of the function.
  • Use local data (working memory), or protect global data by making local copies of global data.
  • Non-reentrant functions are not called.

3. Reentrancy and thread safety

In general, reentrant functions must be thread-safe, but not vice versa. Without locking, if a function uses global or static variables, it is not thread-safe or reentrant. If we improve it and lock the access to global variables, it is thread-safe but not reentrant, because the usual way of locking is for access from different threads (such as Java's synchronized), when the same thread has multiple accesses There will be a problem with the next visit. A function is reentrant only if it satisfies the four conditions for reentrancy.

Let's look back at synchronized, which has an internal locking mechanism that enforces atomicity and is a reentrant lock. Therefore, when a thread uses the synchronized method to call another synchronized method of the object, that is, a thread obtains an object lock and then requests the object lock again, and the lock can always be obtained.

In Java, when the same thread calls other synchronized methods/blocks in its own class, it will not block the execution of the thread. The same thread is reentrant to the same object lock, and the same thread can acquire the same lock multiple times. That is, it can be reentrant multiple times. The reason is that the operation of thread acquiring object lock in Java is in units of threads, not in units of calls.

Five Summary

Synchronized is the most commonly used way to ensure thread safety in Java concurrent programming, and its use is relatively simple. However, if we can deeply understand its principle and understand the underlying knowledge such as monitor locks, on the one hand, it can help us use the Synchronized keyword correctly, on the other hand, it can also help us better understand the concurrent programming mechanism, which will help us in Under different circumstances, choose a more optimal concurrency strategy to complete the task. It can also calmly deal with various concurrency problems encountered in ordinary times.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325129099&siteId=291194637