Mainly explain the application of synchronized and memory semantics.
preface
Before reading this article, I recommend that you read my previous article “Java Concurrent Programming series 1- Basics”, otherwise the relevant knowledge will not understand, especially concurrency related visibility, order, and memory model JMM, etc.
In Java, the keyword synchronized ensures that only one thread can execute a method or block of code at any one time. It is also important to note that synchronized has another important function. Synchronized ensures that changes made by one thread (primarily changes in shared data) are seen by other threads (guaranteed visibility, a complete substitute for Volatile).
Three applications of synchronized
The three main applications of synchronized are as follows:
- Modify instance method, function in the current instance lock, enter the synchronization code to obtain the current instance lock;
- Modify static methods that lock the current class object before entering the synchronized code to obtain the current class object lock;
- Modifies a block of code that specifies a lock object, locks a given object, and acquires the lock for the given object before entering a synchronized code base.
Synchronized acts on instance methods
Synchronized is used to modify instance methods in an instance object. Synchronized does not include static methods.
public class AccountingSync implements Runnable {
// Share resources (critical resources)
static int i = 0;
// synchronized modifies instance methods
public synchronized void increase(a) {
i ++;
}
@Override
public void run(a) {
for(int j=0; j<1000000; j++){
increase();
}
}
public static void main(String args[]) throws InterruptedException {
AccountingSync instance = new AccountingSync();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("static, i output:" + i);
}
}
/ * *
* Output result:
* static, i output:2000000
* /
Copy the code
If synchronized is not added before increase(), since i++ does not have atomicity, the final result will be less than 2000000. See Java concurrent programming series 2-volatile. This is very important:
An object has only one lock. When a thread acquires the lock of the object, other threads cannot acquire the lock and therefore cannot access other synchronized instance methods of the object. However, other threads can still access other non-synchronized methods of the object.
However, one thread A needs to access the synchronized method f1 of obj1 (the current lock is obj1), and another thread B needs to access the synchronized method f2 of obj2 (the current lock is obj2), which is allowed:
public class AccountingSyncBad implements Runnable {
// Share resources (critical resources)
static int i = 0;
// synchronized modifies instance methods
public synchronized void increase(a) {
i ++;
}
@Override
public void run(a) {
for(int j=0; j<1000000; j++){
increase();
}
}
public static void main(String args[]) throws InterruptedException {
// new two new AccountingSync instances
Thread t1 = new Thread(new AccountingSyncBad());
Thread t2 = new Thread(new AccountingSyncBad());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("static, i output:" + i);
}
}
/ * *
* Output result:
* static, i output:1224617
* /
Copy the code
The difference is that we create two new instances AccountingSyncBad at the same time, and then start two different threads to operate on the shared variable I. Unfortunately, the result is 1224617 instead of 2000000, because the code made a serious error. Although we modify the increase method with synchronized, two different instance objects are new, which means that there are two different instance object locks, so T1 and T2 enter their own object locks, which means that t1 and T2 threads use different locks, so thread safety cannot be guaranteed.
Each object has an object lock, and the locks of different objects do not affect each other.
The solution to this dilemma is to use synchronized on a static increase method, in which case the object lock is unique to the current class object, since no matter how many instance objects are created, there is only one for the class object. Let’s take a look at using a static increase method that applies synchronized.
Synchronized acts on static methods
Synchronized, when applied to a static method, is the class lock of the current class and does not belong to an object.
If the current class lock is acquired, it does not affect the object lock.
Because static members are not exclusive to any instance object and are class members, concurrent operations on static members can be controlled through class object locks. Note that if thread A calls the non-static synchronized method of an instance object, and thread B calls the static synchronized method of the class that the instance object belongs to, mutual exclusion will not occur. The lock used to access a static synchronized method is the current class object, and the lock used to access a non-static synchronized method is the current instance object lock.
public class AccountingSyncClass implements Runnable {
static int i = 0;
/ * *
* For static methods, the lock is the current class object, i.e
* AccountingSyncClass The corresponding class object of the class
* /
public static synchronized void increase(a) {
i++;
}
// Non-static, access to different locks will not be mutually exclusive
public synchronized void increase4Obj(a) {
i++;
}
@Override
public void run(a) {
for(int j=0; j<1000000; j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
/ / new new instances
Thread t1=new Thread(new AccountingSyncClass());
/ / new new instances
Thread t2=new Thread(new AccountingSyncClass());
// Start the thread
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(i);
}
}
/ * *
* Output result:
* 2000000
* /
Copy the code
Because the synchronized keyword modifies static increase methods, its lock object is the class object of the current class, unlike the synchronized method that modifies instance methods. Note that the increase4Obj method in this code is an instance method whose object lock is the current instance object. If it is called by another thread, there will be no mutual exclusion (lock objects are different, after all), but we should be aware that thread-safety issues can be found in this case (handling the shared static variable I).
Synchronized synchronized code block
In some cases, we can write the method body is bigger, at the same time there are some more time-consuming operation, and need to be synchronized code and only a small part, if directly with the method of synchronous operation, may do more harm than good, the way we can use the synchronized code block to package needs to be synchronized code, This eliminates the need to synchronize the entire method. An example of a block of synchronized code is as follows:
public class AccountingSync2 implements Runnable {
static AccountingSync2 instance = new AccountingSync2(); // create a singleton
static int i=0;
@Override
public void run(a) {
// Omit other time-consuming operations....
// use the synchronization block to synchronize variable I with the lock object instance
synchronized(instance){
for(int j=0; j<1000000; j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(i);
}
}
/ * *
* Output result:
* 2000000
* /
Copy the code
It can be seen from the code that synchronized is applied to a given instance object, that is, the current instance object is the lock object. Each time a thread enters the code block wrapped by synchronized, the current thread is required to hold the instance object lock. If other threads currently hold the lock, New threads must wait, ensuring that only one thread executes i++ at a time; Operation. In addition to instance as an object, we can also use this object (representing the current instance) or the current class object as the lock, as follows:
//this, the current instance object lock
synchronized(this) {
for(int j=0; j<1000000; j++){
i++;
}
}
/ / class object lock
synchronized(AccountingSync.class){
for(int j=0; j<1000000; j++){
i++;
}
}
Copy the code
Synchronized disables instruction rearrangement analysis
For instructions to rearrange, see the article Java Concurrent Programming Series 1- Basics
Let’s start with the following code:
class MonitorExample {
int a = 0;
public synchronized void writer(a) { / / 1
a++; / / 2
} / / 3
public synchronized void reader(a) { / / 4
int i = a; / / 5
/ /...
} / / 6
}
Copy the code
Suppose thread A executes the writer() method, followed by thread B which executes the Reader () method. According to the happens-before rule, the happens-before relationships involved in this process can be divided into two categories:
- 1 happens before 2, 2 happens before 3; 4. What happens before 5?
- 3 happens before 4 according to the monitor lock rule.
- 2 happens before 5.
The graphical representation of the happens-before relationship above is as follows:
In the figure above, each arrow links two nodes, representing a happens-before relationship. Black arrows indicate program order rules; The orange arrow represents the monitor lock rule; The blue arrows represent the happens-before guarantees provided by combining these rules.
The figure above shows that after thread A releases the lock, thread B subsequently acquires the same lock. In the figure above, 2 happens before 5. Therefore, all shared variables visible to thread A before the lock is released will immediately become visible to thread B after thread B acquires the same lock.
Reentrancy of synchronized
In terms of mutex design, when a thread attempts to manipulate a critical resource of an object lock held by another thread, it will block, but when a thread requests the critical resource of an object lock held by itself again, this situation is a reentrant lock and the request will succeed.
Synchronized is a reentrant lock, so it is allowed for a thread to call another synchronized method within its method body at the same time as calling the object:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run(a) {
for(int j=0; j<1000000; j++){
//this, the current instance object lock
synchronized(this) {
i++;
increase();// Synchronized reentrancy
}
}
}
public synchronized void increase(a){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(i);
}
}
Copy the code
When the current instance object is locked and enters the synchronized code block to execute the synchronized code, and another synchronized method of the current instance object is called in the code block, the request for the current instance lock will be allowed. Special attention is paid to the fact that when a subclass inherits from its parent, it can also call its parent’s synchronized methods via a reentrant lock. Note that since synchronized is implemented based on monitor, the counter in Monitor is still incremented by one with each reentrant.
conclusion
This article explains three kinds of application of synchronized, analysis of command rearrangement, and reentrancy of synchronized. Through this article, you can basically master the use posture of synchronized, and the pits that may be encountered. About “thread interrupt and synchronized” related knowledge, because of space reasons will not write, you can check the relevant information online, further learning.
Reference materials: In-depth Understanding of Java Memory Model, Practical Java Concurrent Programming
Welcome everyone to like a lot, more articles, please pay attention to the wechat public number “Lou Zai advanced road”, point attention, do not get lost ~~