The teacher has sent something, you have to queue up to receive it, Java synchronized keyword

When I was a child, when the teacher sent something, he asked us to line up one by one not to grab it, or we would get spanked.

This scenario of multiple people queuing up to receive things is very similar to the scenario in which multiple threads access shared resources in programming. Today we combine the Java synchronized keyword to explain.

1. When will thread safety issues arise?

There is no thread safety problem in a single thread, but in multi-threaded programming, it is possible to access the same resource at the same time. This resource can be various types of resources: a variable, an object, a file , a database table, etc., and when multiple threads access the same resource at the same time, there will be a problem:

Since the process executed by each thread is uncontrollable, it is likely to cause the final result to go against the actual desire or directly lead to a program error.

Take a simple example:

Now there are two threads respectively reading data from the network, and then inserting into a database table, requiring no duplicate data to be inserted.

Then there must be two operations in the process of inserting data:

1) Check whether the data exists in the database;

2) If it exists, it is not inserted; if it does not exist, it is inserted into the database.

If two threads are represented by thread-1 and thread-2 respectively, and at a certain moment, both thread-1 and thread-2 read data X, then this may happen:

thread-1 checks whether data X exists in the database, and then thread-2 goes on to check whether data X exists in the database.

As a result, the result of the two threads checking is that the data X does not exist in the database, then the two threads insert the data X into the database table respectively.

This is a thread safety issue, that is, when multiple threads access a resource at the same time, the result of the program running will not be the result you want to see.

Here, this resource is called: critical resource (also called shared resource).

That is, thread safety issues may arise when multiple threads simultaneously access critical resources (an object, a property in an object, a file, a database, etc.).

However, when multiple threads execute a method, the local variables inside the method are not critical resources, because the method is executed on the stack, and the Java stack is thread-private, so there is no thread safety problem.

2. How to solve the thread safety problem?

Basically, all concurrent modes adopt the scheme of "serialized access to critical resources" when solving the problem of thread safety, that is, only one thread can access critical resources at the same time, which is also called synchronous mutual exclusive access.

Generally speaking, a lock is added in front of the code that accesses the critical resource, and the lock is released after accessing the critical resource, allowing other threads to continue to access.

In Java, there are two ways to achieve synchronous mutual exclusion access: synchronized and Lock.

This article mainly describes the use of synchronized, and the use of Lock will be discussed later.

3, synchronized synchronization method, synchronized block

Let's first look at a concept: mutual exclusion locks, as the name suggests: locks that can achieve the purpose of mutually exclusive access.

Take a simple example: if a mutex is added to a critical resource, when a thread is accessing the critical resource, other threads can only wait.

In Java, each object has a lock marker (monitor), also known as a monitor. When multiple threads access an object at the same time, the thread can only access it if it acquires the lock of the object.

In Java, you can use the synchronized keyword to mark a method or code block. When a thread calls the synchronized method of the object or accesses the synchronized code block, the thread acquires the lock of the object, and other threads cannot access the object temporarily. method, this thread will release the lock of the object only after waiting for the completion of the execution of the method or the execution of the code block, and other threads can execute the method or code block.

Here are a few simple examples to illustrate the use of the synchronized keyword:

① synchronized method

In the following code, two threads respectively call the insertData object to insert data:

public class Test {

	public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
         
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
         
         
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
    }  
}
 
class InsertData {
    private List<Integer> list = new ArrayList<Integer>();
     
    public void insert(Thread thread){
        for(int i=0; i<5; i++){
            System.out.println("thread "+thread.getName()+" inserting data"+i);
            list.add(i);
        }
    }
}

It can be seen from the output that the two threads are executing the insert method at the same time, and there is no mutual waiting.

thread Thread-0 inserting data 0
 thread Thread-1 inserting data 0
 thread Thread-0 inserting data 1
 thread Thread-1 inserting data 1
 thread Thread-0 inserting data 2
 thread Thread-0 inserting data 3
 thread Thread-1 inserting data 2
 thread Thread-0 inserting data 4
 thread Thread-1 inserting data 3
 thread Thread-1 inserting data 4

Next we add the keyword synchronized in front of the insert method:

class InsertData {
    private List<Integer> list = new ArrayList<Integer>();
     
    public synchronized void insert(Thread thread){
        for(int i=0; i<5; i++){
            System.out.println("thread "+thread.getName()+" inserting data"+i);
            list.add(i);
        }
    }
}

The output results can be seen: Thread-1 inserts data until Thread-0 finishes inserting data. Explain that Thread-0 and Thread-1 execute the insert method sequentially.

thread Thread-0 inserting data 0
 thread Thread-0 inserting data 1
 thread Thread-0 inserting data 2
 thread Thread-0 inserting data 3
 thread Thread-0 inserting data 4
 thread Thread-1 inserting data 0
 thread Thread-1 inserting data 1
 thread Thread-1 inserting data 2
 thread Thread-1 inserting data 3
 thread Thread-1 inserting data 4

Notice:

1) When a thread is accessing a synchronized method of an object, then other threads cannot access other synchronized methods of that object. The reason for this is very simple, because an object has only one lock. When a thread acquires the lock of the object, other threads cannot acquire the lock of the object, so other synchronized methods of the object cannot be accessed.

2) When a thread is accessing the synchronized method of an object, then other threads can access the non-synchronized method of the object. The reason for this is very simple. Accessing a non-synchronized method does not require obtaining the lock of the object. If a method is not modified with the synchronized keyword, it means that it will not use critical resources, then other threads can access this method.

3) If a thread A needs to access the synchronized method fun1 of the object object1, and another thread B needs to access the synchronized method fun1 of the object object2, even if object1 and object2 are of the same type), there will be no thread safety problem, because they access the different objects, so there is no mutual exclusion problem.

② synchronized code block

A synchronized block of code looks like this:

synchronized(synObject) {

}

When executing this code block in a thread, the thread will acquire the lock of the object synObject, so that other threads cannot access the code block at the same time.

synObject can be this, which means acquiring the lock of the current object, or a property in the class, which means acquiring the lock of the property.

For example, the above insert method can be changed to the following two forms:

class InsertData {
    private List<Integer> list = new ArrayList<Integer>();
     
    public void insert(Thread thread){
    	synchronized (this) {
	        for(int i=0; i<5; i++){
	            System.out.println("thread "+thread.getName()+" inserting data"+i);
	            list.add(i);
	        }
    	}
    }
}
class InsertData {
    private List<Integer> list = new ArrayList<Integer>();
    private Object object = new Object();
    
    public void insert(Thread thread){
    	synchronized (object) {
	        for(int i=0; i<5; i++){
	            System.out.println("thread "+thread.getName()+" inserting data"+i);
	            list.add(i);
	        }
    	}
    }
}

As can be seen from the above, synchronized code blocks are much more flexible to use than synchronized methods. Because maybe only a part of the code in a method only needs to be synchronized, if the entire method is synchronized with synchronized at this time, it will affect the efficiency of program execution. This problem can be avoided by using synchronized code blocks, which can achieve synchronization only where synchronization is required.

In addition, each class will also have a lock, which can be used to control concurrent access to static data members.

And if a thread executes a non-static synchronized method of an object, another thread needs to execute the static synchronized method of the class to which the object belongs, and mutual exclusion will not occur at this time, because accessing the static synchronized method occupies the class lock, while accessing the non-static synchronized method. The static synchronized method occupies the object lock, so there is no mutual exclusion.

Look at the following code to understand:

public class Test {

	public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            @Override
            public void run() {
                insertData.insert(Thread.currentThread());
            }
        }.start(); 
        new Thread(){
            @Override
            public void run() {
                insertData.insert1(Thread.currentThread());
            }
        }.start();
    }  
}
 
class InsertData { 
    public synchronized void insert(Thread t){
        System.out.println("thread "+t.getName()+" implement insert start");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread "+t.getName()+" implement insert Finish");
    }
     
    public synchronized static void insert1(Thread t) {
    	System.out.println("thread "+t.getName()+" implement insert1 start");
    	System.out.println("thread "+t.getName()+" implement insert1 Finish");
    }
}

The output results can be seen: the first thread executes the insert method, which will not cause the second thread to block the execution of the insert1 method. The two threads do not use the same lock, so there will be no synchronization blocking.

thread Thread-0 implement insert start
 thread Thread-1 implement insert1 start
 thread Thread-1 implement insert1 Finish
 thread Thread-0 implement insert Finish

Let's take a look at what the synchronized keyword does. Let's decompile its bytecode. The decompiled bytecode of the following code is:

package com.testsyn;

public class InsertData {
	private Object object = new Object();
    
    public void insert1(Thread thread){
    	//property object lock
        synchronized (object) {
         
        }
    }
    
    public void insert2(Thread thread){
    	//current object lock
    	synchronized (this) {
    		
    	}
    }
    
    public void insert3(Thread thread){
    	//class lock
    	synchronized (InsertData.class) {
    		
    	}
    }
     
    //static method lock
    public static synchronized void insert4(Thread thread){
         
    }
     
    //Non-static method lock
    public synchronized void insert5(Thread thread){
    	
    }
    
    //no lock
    public void insert6(Thread thread){
    	
    }
}

Execute the command javap -v InsertData.class on the generated bytecode file, and the result is as follows:

It can be seen from the bytecode obtained by decompilation that the synchronized code block actually has two more instructions, monitorenter and monitorexit. When the monitorenter instruction is executed, the lock count is increased by 1, and when the monitorexit instruction is executed, the lock count is decreased by 1. In fact, this is very similar to the PV operation in the operating system. The PV operation in the operating system is used to control multiple threads to critical access to resources. For a synchronized method, the executing thread identifies whether the method_info structure of the method has the ACC_SYNCHRONIZED, ACC_STATIC flags set, then it automatically acquires the object's lock or class lock, calls the method, and finally releases the lock. If an exception occurs, the thread automatically releases the lock.

Note: For synchronized methods or synchronized code blocks, when an exception occurs, the JVM will automatically release the lock occupied by the current thread, so there will be no deadlock caused by the exception.

Welcome friends to leave a message and exchange~~
To browse more articles, you can follow the WeChat public account: diggkr

Tags: Java Multithreading synchronized

Posted by eastcoastdubs on Tue, 24 May 2022 03:06:35 +0300