1. Basic use of Synchronized
Synchronized is the most commonly used method to solve concurrency problems in Java and the easiest method. Synchronized has three main functions: (1) Ensure thread mutually exclusive access synchronization code (2) Ensure that the modification of shared variables can be timely visible (3) Effectively solve the reordering problem. Synchronized has three uses of Synchronized:
(1) Ordinary method of modifying
(2) Modify static methods
(3) Modify the code block
Next, I will use a few example programs to illustrate these three usage methods (for the sake of comparison, except for the different usage methods of Synchronized, the other three codes are basically consistent).
1. No synchronization:
Code Snippet 1:
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 executes first. In this process, Thread 1 and Thread 2 execute at the same time.
Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end
2. Synchronize 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. After comparing it with the code segment, it can be clearly seen that thread 2 needs to wait for the execution of method1 of thread 1 to complete before starting to execute the method2 method.
Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end
3. Static method (class) synchronization
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 result is as follows. The synchronization of static methods is essentially a synchronization of classes (static methods are essentially methods of class, not methods on objects). Therefore, even if test and test2 belong to different objects, they both belong to instances of the SynchronizedTest class, so method1 and method2 can only be executed sequentially, and cannot be executed 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 and start execution, thread 2 needs to wait for the synchronization block execution 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 still have any questions about the above execution results, don't worry. Let's first understand the principle of Synchronized, and then look back at the above questions to see at a glance. Let’s first look at how Synchronized code synchronizes the 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 owners the monitor associated with objectref, it reenters the monitor, increasing 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 obtain ownership of the monitor. The process is as follows:
1. If the number of entry of the monitor is 0, the thread enters the monitor, and then sets the entry number to 1, the thread is the owner of the monitor.
2. If the thread already owns the monitor and just re-enteres, the number of entry into the monitor is added to 1.
3. If other threads have occupied the monitor, the thread enters a blocking state until the number of entry of the monitor is 0, and then try to obtain ownership of the monitor again.
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 number of the monitor entering is reduced by 1. If the number of the monitor entering is 0 after decrementing by 1, the thread exits the monitor and is no longer the owner of this monitor. Other threads blocked by this monitor can try to obtain ownership of this monitor.
Through these two paragraphs of description, we should be able to clearly see the implementation principle of Synchronized. The semantic underlying layer of Synchronized is completed through a monitor object. In fact, wait/notify and other methods also rely on monitor objects. This is why only methods such as wait/notify can be called in synchronized blocks or methods, otherwise an exception of java.lang.IllegalMonitorStateException will be thrown.
Let's look at the decompilation results 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:
Judging from the results of decompilation, the synchronization of the method is not completed through the instructions monitorenter and monitorexit (in theory, it can also be implemented through these two instructions). However, compared with ordinary methods, the ACC_SYNCHRONIZED identifier is added to its constant pool. JVM implements the synchronization of methods 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 set, the execution thread will first obtain the monitor, and then execute the method body after the method is successfully executed. After the method is executed, the monitor will be released. During method execution, no other thread can get the same monitor object anymore. In fact, there is no difference in essence, but the synchronization of the method is an implicit way to achieve it without the need to be done through bytecode.
3. Explanation of operation results
With an understanding of the principle of Synchronized, you can easily solve it by looking at the above program.
1. Code segment 2 results:
Although method1 and method2 are different methods, both 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, so you can only obtain the locks mutually exclusively. Therefore, 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 method1 and method2 both belong to static synchronization methods, you need to get the monitor on the same class (each class only corresponds to one class object), so you can only execute sequentially.
3. Code segment 4 results:
For the synchronization of code blocks, it is essentially necessary to obtain the monitor of the object in the brackets after the Synchronized keyword. Since the contents of the brackets in this code are this, and method1 and method2 are called through the same object, so before entering the synchronization block, you need to compete for the locks on the same object, so the synchronization block can only be executed in sequence.
Four summary
Synchronized is the most commonly used method for thread safety in Java concurrent programming, and it is relatively simple to use. However, if we can understand its principles in depth and have some understanding of underlying knowledge such as monitor locks, it can help us correctly use the Synchronized keywords, and on the other hand, it can also help us better understand the concurrency programming mechanism, help us choose better concurrency strategies to complete tasks under different circumstances. You can also calmly deal with various concurrent problems you encounter in daily life.