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 ~~