Concurrency problems occur when multiple threads simultaneously read and write shared resources. Java offers several ways around this, of which the synchronized keyword is the most common and easiest. Synchronized locks a shared resource by using a mutex so that only one thread can access and modify it at a time, and the other threads must wait. After the current thread is modified and the mutex is released, other threads can access it.
This paper first introduces the occurrence of multithreading concurrency problems, then explains how to use synchronized, and finally briefly analyzes the realization principle of synchronized.
Concurrency problems and their three scenarios
When multiple threads simultaneously read and write the same variable in the memory, concurrency problems may occur due to operating system thread scheduling, kernel cache invisibility, and instruction reordering, making the execution results unpredictable. For details, see concurrency problems and their causes.
Depending on the type and location of the variable, concurrency problems can occur in one of three scenarios:
- Static variable, multithreaded access to the same instance of the class
- Static variables, multithreaded access to different instances of the class
- Instance member variable. Multiple threads access the same instance
A static variable shared by different instance objects of a class. When multiple threads operate on instances of a class, they operate on this variable at the same time, in two ways: 1. Multiple threads operate on the same instance of a class, and adding an instance object level lock (for example, synchronized) can do the trick. 2. Multiple threads operate on different instances of a class, requiring class-level locks, such as adding synchronized to static methods.
Member variable, instance object private. When multiple threads operate on the same instance and its code block or method is not locked, the variable can be read and written simultaneously, causing concurrency problems. In other cases, there are no concurrency issues, such as: 1. 2. Multiple threads share an instance object, but methods or code blocks that manipulate properties are locked.
The effect of synchronized
Multiple threads reading and writing shared variables at the same time will cause concurrency problems. An easy way to solve this problem is to control that only one thread can access the shared variables at the same time, and other threads have to wait for the shared variables to be idle before they can access them. Java provides the synchronized keyword to achieve this function.
Synchronized ensures thread-safety by adding mutex to methods or blocks of code. Multiple threads holding the same lock, and only one thread at a time can take the lock and execute the block or method that locks it. When they concurrently access a lock-modified method or block of synchronized code, only one thread can acquire the lock and access it at a time. Other threads that do not acquire the lock must wait for the lock to be released before they can execute. This ensures that only one of the different threads holding the same lock can execute at a time. Synchronized modified methods and blocks of code release the lock either after normal execution or when an exception is thrown.
Synchronized uses two types of locks: object locks and class locks. Object locking is the use of an instance object as the lock, according to the location of the lock is divided into instance method locking (default lock object is this, that is, the current instance object) and synchronous code block locking (specified lock object). Class locking is the use of class. Class as a lock, according to the use of location, divided into static method locking (the default lock object is class, that is, the class of the current class) and synchronous code block locking (specify their own class object).
- Each instance should have its own lock (
this
), different instances do not affect each other; - The lock object is
*.class
As well assynchronized
Modification isstatic
Method, all objects of the class share this lock;
There are three main functions of synchronized:
- Atomicity: access synchronization code that ensures threads are mutually exclusive;
- Visibility: Changes to a shared variable must be visible in the main memory before an unlock operation is performed. If you lock a variable, the value of the variable will be emptied from working memory, and the variable will need to be reloaded from main memory before any other thread can use it.
- Order: Effectively solve the reordering problem, that is, an UNLOCK operation occurs first before a lock operation facing the same lock.
Three ways to use synchronized
Synchronized can be used in the following three ways:
- Modifier instance method: Use the current instance object as the lock, the lock of the current instance object before entering the synchronization code. Scenario 1 and Scenario 3 are solved.
- Modify static methods: Using the current class as a lock, the lock of the current class object is acquired before entering the synchronized code. Scenario 1 and Scenario 2 can be solved.
- Modify code block: you specify the lock object, can be an object lock, can also be a class lock, to enter the synchronization code block to obtain the specified lock.
Applies to instance methods
When multiple threads access an instance method, and the method reads and writes static variables or instance member variables of the same instance, the synchronized modifier can be used to solve the problem.
At this point, multiple threads simultaneously hold the current object as a lock, and synchronized converts the whole method into synchronized code. Only one thread can hold the lock of the instance object at a time and enter the method execution. Until the lock is released by the current thread, other threads are blocked and cannot enter the method execution, ensuring thread-safety.
Note: The usage scenario must be multiple threads operating on the same instance. If multiple threads operate on different instance objects, there is no thread-safety problem for instance member variables, while for static variables, the object locks of different threads are different, and different threads can still execute concurrently, so there is no guarantee of thread-safety.
public class SynchronizedExample implements Runnable {
static int m = 0;
int n = 0;
public synchronized void inc(a) {
// The auto-add operation is actually divided into three instructions, not atomic operation, so there are concurrency problems with multi-threaded simultaneous reading and writing
m++;
n++;
}
@Override
public void run(a) {
for (int j = 0; j < 10000; j++) { inc(); }}public static void main(String[] args) throws InterruptedException {
// Multiple threads share the same instance
ProblemExample task = new ProblemExample();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("finish m: " + m + ", n: " + task.n);
// finish m: 20000, n: 20000}}Copy the code
Applied to static methods
Concurrency problems occur when multiple threads simultaneously read and write static variables of a class.
Attention! When multiple threads each use different instances of a class but use the same instance object to lock it, thread safety is not guaranteed.
If multiple threads access the same instance object of a class, the instance object can be used as the lock, or the class object classname.class can be used as the lock.
If multiple threads are accessing different instance objects of a class, only the classname. class object can be used as the lock, not the instance object.
public class SynchronizedExample implements Runnable {
static int m = 0;
public static synchronized void inc(a) {
// The auto-add operation is actually divided into three instructions, not atomic operation, so there are concurrency problems with multi-threaded simultaneous reading and writing
m++;
}
@Override
public void run(a) {
for (int j = 0; j < 10000; j++) { inc(); }}public static void main(String[] args) throws InterruptedException {
// Multiple threads share different instances and can only use class objects as locks
ProblemExample task1 = new ProblemExample();
Thread t1 = new Thread(task1);
ProblemExample task2 = new ProblemExample();
Thread t2 = new Thread(task2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("finish m: " + m);
// finish m: 20000}}Copy the code
Act on a code block
Synchronized can also act on blocks of code inside methods, in addition to instance and static methods. In this case, the lock needs to be specified by itself, which can be an instance object or a class object.
public class SynchronizedExample implements Runnable {
static SynchronizedExample instance = new SynchronizedExample();
static int i = 0;
@Override
public void run(a) {
// 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 = newThread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); }}Copy the code
Apply synchronized to a given instance object, that is, the current instance object is the lock object. Each time a thread enters a block of synchronized wrapped code, the current thread is required to hold the instance lock. If another thread currently holds the lock, The newly arrived thread must wait, ensuring that only one thread performs i++ at a time. Of course, you can also use this or class.
// this, the current instance object lock
synchronized(this) {
for (int j = 0; j < 1000000; j++) { i++; }}// Class object lock
synchronized(SynchronizedExample.class) {
for (int j=0; j<1000000; j++) { i++; }}Copy the code
After introducing the basic meaning and usage of synchronized, the realization principle of synchronized is further analyzed.
Introduction of the principle
The underlying implementation principles for synchronized methods and synchronized code blocks are different.
The synchronized method, compared to the normal method, adds the ACC_SYNCHRONIZED identifier to the constant pool inside the method. When a method is invoked, the calling instruction will check whether the ACC_SYNCHRONIZED access flag of the method is set. If so, the executing thread will acquire monitor first and execute the method body only after the acquisition is successful. Monitor will be released after the method execution is completed. During method execution, no other task thread can retrieve the same Monitor object.
The synchronized code block implements thread exclusion through the Monitorenter and Monitorexit directives.
- Monitorenter: Thread execution
monitorenter
Command attempt to obtainmonitor
Ownership ifmonitor
If the number of entries is 0, the thread entersmonitor
, and then set the number of entries to 1, the thread ismonitor
Owner of. If the thread already owns itmonitor
, just re-enter, then entermonitor
Plus 1. If another thread is occupiedmonitor
, the thread enters the blocking state untilmonitor
Is 0, then try again to obtainmonitor
Ownership of. - Monitorexit: perform
monitorexit
The thread must correspond to the current lock objectmonitor
When the instruction is executed, the number of monitor entries decreases by 1. If the number of monitor entries decreases by 1, the thread exits the monitor and is no longer the owner of the monitor. Other threads blocked by the monitor can try to take ownership of the monitor.
reentrancy
Synchronized is reentrant, and when a thread again requests that it own the critical resource of the object lock, this situation is a reentrant lock, and the request will succeed. Therefore, it is allowed for a thread to call another synchronized method on the same object while calling a synchronized method in its method body. In other words, it is allowed for a thread to obtain an object lock and then request the object lock again, which is the reentrancy of synchronized.
reference
Key words: Synchronized Detailed analysis of Synchronized principle (Ali interview questions)