preface
This series of materials is based on the video of dark horse: java Concurrent Programming I haven't finished watching it yet. On the whole, this is the best concurrent programming video I've ever seen. Here are the notes based on the video.
1. java Memory Model
This part is based on the book "the art of java Concurrent Programming"
1. Two problems of concurrent programming
In concurrent programming, two issues need to be noted:
- How do threads communicate (exchange information)
- How to synchronize threads (threads here refer to active entities executing concurrently)
Communication mechanism:
- Shared memory: in shared memory, threads communicate implicitly through the public state in read-write memory. In other words, all threads share a memory area. The data in this area is updated with the data in the thread memory, and other threads read from it to obtain the latest messages. This model is used in Java
- Message passing: there is no common state between threads. Threads must communicate by sending messages.
Synchronization:
- Synchronization is a mechanism used to control the relative order of operations between different threads in a program. In the shared memory concurrency model, synchronization is displayed
- Programmers need to show that specifying a method or a piece of code needs to be mutually exclusive between threads. In order to achieve unified message reading.
2. Abstract structure of Java Memory Model
In Java, all instance areas, static fields and array elements are stored in heap memory. There are threads in the heap that are shared. Local variables, method definition parameters, and exception handler parameters will not be shared in the thread. These three types will not have the problem of memory visibility, nor will they be affected by the memory model.
JMM: Java Memory Model, which determines that a thread's writing to a shared variable can be interfaced with another thread. In this model, the shared variables between threads are stored in main memory, and each thread has a private local memory, which stores a copy of the shared variables. In short, there is a public space and the private space of each thread. The changes of shared variables by threads first occur in the private space and finally synchronize to the public space. Threads can not directly obtain the variables of the private space of other threads, but can only be read from the public space. The abstract diagram is as follows:
In the figure, it can be seen that if A wants to communicate with B, at least the following steps are required:
- Thread A first writes the data to memory A this time
- Thread A flushes the shared variables in local memory A to main memory
- Thread B goes to main memory to read the shared variables updated by thread A
This process is demonstrated below. Let's assume that there is a variable x=0 at the beginning, then thread 1 Changes x to 1, and then B reads x=1 in main memory:
Conclusion: on the whole, these two steps are actually that thread A is sending messages to thread B, and the communication process must go through main memory. JMM provides memory visibility assurance for Java programmers by controlling the interaction between main memory and local memory of each thread.
2. Problem analysis
Now that you know the JMM memory model and the communication between threads, some errors will inevitably occur in this model, such as:
- The thread did not have time to refresh the shared variables of local memory to main memory, resulting in other threads not receiving messages
The following code is used to reflect this error. There are two threads, one + 5000 times and one - 5000 times, but the result is not 0:
@Slf4j public class Test1 { static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for(int i = 0; i < 5000; i++){ count ++; } }, "t1"); Thread t2 = new Thread(()->{ for(int i = 0; i < 5000; i++){ count --; } }, "t2"); t1.start(); t2.start(); //t2 wait for t1 after running first t1.join(); //t1 runs first and waits for t2 t2.join(); log.debug("{}", count); //DEBUG [main] (12:40:47,384) (Test1.java:33) - -206 } }
Problem analysis:
The above results may be positive, negative or zero. Why? Because the self increment and self decrement of static variables in Java are not atomic operations, they must be analyzed from bytecode to fully understand them
For example, for i + + (i is a static variable), the following JVM bytecode instructions will actually be generated:
getstatic i // Gets the value of the static variable i iconst_1 // Prepare constant 1 iadd // Self increasing putstatic i // Store the modified value into the static variable i getstatic i // Gets the value of the static variable i iconst_1 // Prepare constant 1 isub // Self subtraction putstatic i // Store the modified value into the static variable i
The memory model of Java is as follows. To complete the self increase and self decrease of static variables, data exchange needs to be carried out in main memory and working memory:
There is no problem when the above operations are executed under a single thread, because there will be no problem of shared memory, but it will not work under multithreading. Some problems caused by trying to share memory will occur.
Under single thread:
A negative number occurs in multithreading: the result of thread 2 overwrites that of thread 1
A positive number occurs in multithreading: the result of thread 1 overrides that of thread 2
A summary: the reason for the above situation is that the thread did not come and synchronized its own data to main memory, resulting in overwriting the synchronized data of the next thread.
3. Critical zone and race condition
1 Critical Section
- There is no problem for a program to run multiple threads
- The problem is that multiple threads access shared resources
1. There's no problem with multiple threads just accessing
2. The main problem is writing to shared resources - If there are multithreaded read-write operations on shared resources in a code block, this code block is called a critical area
For example, the critical area in the following code:
static int counter = 0; static void increment() // Critical zone { counter++; } static void decrement() // Critical zone { counter--; }
2 race conditions
When multiple threads execute in the critical area, the results cannot be predicted due to different execution sequences of codes, which is called race condition
4. Solutions
1. synchronized
In order to avoid the competitive conditions in the critical zone, there are many means to achieve the goal.
- Blocking solution: synchronized, Lock
- Non blocking solution: atomic class
Use synchronized to solve the above problems, commonly known as the [object lock], which uses a mutually exclusive way to make at most one thread hold the [object lock] at the same time, and other threads will block when they want to obtain the [object lock]. This ensures that the thread with the lock can safely execute the code in the critical area without worrying about thread context switching.
By analogy, there is a room. Threads are people one by one. When a person enters the room, he locks the room, and others can't enter. When the people in the room finish everything, open the door and the next person enters.
be careful
Although the synchronized keyword can be used to complete mutual exclusion and synchronization in java, they are different:
- Mutual exclusion is to ensure that the race condition of the critical area occurs. Only one thread can execute the code of the critical area at the same time
- Synchronization is due to the different execution order of threads. One thread needs to wait for other threads to run to a certain point
2. Usage
synchronized(object){ //code }
Code to solve this problem using synchronized:
@Slf4j public class Test1 { static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for(int i = 0; i < 5000; i++){ synchronized (Test1.class){ count ++; } } }, "t1"); Thread t2 = new Thread(()->{ for(int i = 0; i < 5000; i++){ synchronized (Test1.class){ count --; } } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}", count); //DEBUG [main] (12:40:47,384) (Test1.java:33) - -206 } }
Explanation:
- Using the above figure to explain, when you enter the room at the beginning, you get the lock, synchronized(room), and others are locked out of the door
- When the second person wants to enter, he finds that the lock has not been obtained and can only wait for the first person to open the door
- Although the CPU time slice runs out at this time, it doesn't matter, because the lock is still in the hands of the first person, and the second person can't get in if he wants to come in. He can only continue after the first person is assigned to the time slice execution and releases the lock
It can be seen that even if thread 1 has a CPU time slice at this time, it is useless, because the lock is still in the hands of thread 2, and thread 1 cannot execute. Thread 1 can start execution only after thread 2 is allocated to the time slice again and the lock is released, so as to ensure that the sequence is not disturbed.
3. Understanding
synchronized actually uses object locks to ensure the atomicity of the code in the critical area. The code in the critical area is inseparable externally and will not be interrupted by thread switching. Then consider the following questions:
-
If synchronized(obj) is placed outside the for loop, how to understand it?
a. On the outside, 5000 * 4 = 20000 instructions will not be interrupted by other threads
b. In other words, if you put it outside, you will execute + + 5000 times first and then 5000 times – -
What happens if t1 synchronized(obj1) and t2 synchronized(obj2)?
a. Different locks mean that two threads can operate on the same shared data at the same time, because they are different locks
b. This leads to the possibility of a situation other than 0
c. The same object lock must be added -
What happens if T1 is synchronized (obj) and t2 is not added? How to understand?
a. If t2 is not added, you will not try to obtain the lock. Similarly, t2 will be modified directly, which is unsafe
4. Use object-oriented to transform code
Encapsulate the added method and the reduced method into an object
//Change to object-oriented encapsulation @Slf4j public class Test1 { static int count = 0; public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(()->{ room.increment(); }, "t1"); Thread t2 = new Thread(()->{ room.decrement(); }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}", room.getCount()); //DEBUG [main] (12:40:47,384) (Test1.java:33) - -206 } } class Room{ private int count = 0; public void increment(){ synchronized (this){ count ++; } } public void decrement(){ synchronized (this){ count --; } } public int getCount(){ synchronized (this){ return count; } } }
5. synchronized is added to the method
- Member method: equivalent to locking the current object
- Static method: equivalent to locking the current class
- The method of not synchronizing is like people who don't obey the rules and don't queue honestly (like going through the window)
class Test{ //Load member method public synchronized void test() { } } //Equivalent to class Test{ public void test() { //Lock current object synchronized(this) { } } } //------------------------------------------------------------------------------------------------ class Test{ //Static method public synchronized static void test() { } } // Equivalent to class Test{ public static void test() { //Lock the current class synchronized(Test.class) { } } }
6. Thread 8 lock
1. First one then two or first two then one
Reason: all are member methods. The same object is locked n
@Slf4j public class EightLock_1 { public static void main(String[] args) { Number n = new Number(); new Thread(()->{ log.debug("begin"); n.a(); }, "t1").start(); new Thread(()->{ log.debug("begin"); n.b(); }, "t2").start(); } //DEBUG [t1] (23:59:21,334) (EightLock_1.java:16) - begin //DEBUG [t2] (23:59:21,334) (EightLock_1.java:20) - begin //DEBUG [t1] (23:59:21,338) (EightLock_1.java:30) - 1 //DEBUG [t2] (23:59:21,338) (EightLock_1.java:34) - 2 } @Slf4j class Number{ //Member method that locks the this object public synchronized void a(){ log.debug("1"); } //Member method, the lock is this object, and a lock is an object public synchronized void b(){ log.debug("2"); } }
2. 12 after 1 second, or 2 and then 1 after 1 second
Reason: if it is t1, execute it first, sleep for one second, then print 1 and release the lock. At this time, the thread gets the lock and prints 2 in an instant. If t2 executes first, print 2 first, and thread 1 sleeps for one second before printing 1
/** * @author * @Description * @verdion * @date 2021/9/7 23:56 */ @Slf4j public class EightLock_1 { public static void main(String[] args) { Number n = new Number(); new Thread(()->{ log.debug("begin"); n.a(); }, "t1").start(); new Thread(()->{ log.debug("begin"); n.b(); }, "t2").start(); } //DEBUG [t1] (00:02:04,551) (EightLock_1.java:17) - begin //DEBUG [t2] (00:02:04,551) (EightLock_1.java:21) - begin //DEBUG [t1] (00:02:05,570) (EightLock_1.java:37) - 1 //DEBUG [t2] (00:02:05,570) (EightLock_1.java:41) - 2 } @Slf4j class Number{ //Member method, which locks the this object public synchronized void a(){ //sleep will not release the lock after sleeping for one second sleep.mySleep(1); log.debug("1"); } //Member method, the lock is this object, and a lock is an object public synchronized void b(){ log.debug("2"); } }
3. Add a common method
The common method does not need to take the lock and executes synchronously
- 3 1s 12
- 23 1s 1
- 32 1s 1
@Slf4j public class EightLock_1 { public static void main(String[] args) { Number n = new Number(); new Thread(()->{ log.debug("begin"); n.a(); }, "t1").start(); new Thread(()->{ log.debug("begin"); n.b(); }, "t2").start(); new Thread(()->{ log.debug("begin"); n.c(); }, "t3").start(); } //DEBUG [t1] (00:08:28,981) (EightLock_1.java:17) - begin //DEBUG [t3] (00:08:28,981) (EightLock_1.java:26) - begin //DEBUG [t2] (00:08:28,981) (EightLock_1.java:21) - begin //DEBUG [t3] (00:08:28,985) (EightLock_1.java:46) - 3 //DEBUG [t1] (00:08:29,997) (EightLock_1.java:38) - 1 //DEBUG [t2] (00:08:29,999) (EightLock_1.java:42) - 2 // //Process finished with exit code 0 } @Slf4j class Number{ //Member method that locks the this object public synchronized void a(){ //sleep will not release the lock after sleeping for one second sleep.mySleep(1); log.debug("1"); } //Member method, the lock is this object, and a lock is an object public synchronized void b(){ log.debug("2"); } public void c(){ log.debug("3"); } }
4. Different objects are locked
First 2 then 1. Because n1 and n2 execute the method at the same time, 1 sleeps for 1 second. At this time, the syn lock is useless
@Slf4j public class EightLock_1 { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(()->{ log.debug("begin"); n1.a(); }, "t1").start(); new Thread(()->{ log.debug("begin"); n2.b(); }, "t2").start(); } } @Slf4j class Number{ //Member method, which locks the this object public synchronized void a(){ //sleep will not release the lock after sleeping for one second sleep.mySleep(1); log.debug("1"); } public synchronized void b(){ log.debug("2"); } }
5. Static method
First 2 then 1, a thread lock class itself, a thread lock object, and two threads execute at the same time
@Slf4j public class EightLock_1 { public static void main(String[] args) { Number n1 = new Number(); new Thread(()->{ log.debug("begin"); n1.a(); }, "t1").start(); new Thread(()->{ log.debug("begin"); n1.b(); }, "t2").start(); } } @Slf4j class Number{ //Static method, lock current class public static synchronized void a(){ //sleep will not release the lock after sleeping for one second sleep.mySleep(1); log.debug("1"); } public synchronized void b(){ log.debug("2"); } }
6. Lock the current class object
Like 1, both lock the current class object and two static methods. At this time, it returns to the case of 1, and the syn lock takes effect.
@Slf4j public class EightLock_1 { public static void main(String[] args) { Number n1 = new Number(); new Thread(()->{ log.debug("begin"); n1.a(); }, "t1").start(); new Thread(()->{ log.debug("begin"); n1.b(); }, "t2").start(); } } @Slf4j class Number{ //Member method that locks the this object public static synchronized void a(){ //sleep will not release the lock after sleeping for one second sleep.mySleep(1); log.debug("1"); } //Member method that locks the this object public static synchronized void b(){ log.debug("2"); } }
7. Two objects, one thread lock object and one thread lock class
First 2 1s then 1, because the lock is different from the same thing at this time
@Slf4j public class EightLock_1 { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(()->{ log.debug("begin"); //class n1.a(); }, "t1").start(); new Thread(()->{ log.debug("begin"); //Current object n2 n2.b(); }, "t2").start(); } } @Slf4j class Number{ //Static methods, locking classes public static synchronized void a(){ //sleep will not release the lock after sleeping for one second sleep.mySleep(1); log.debug("1"); } //Member method, which locks the this object public synchronized void b(){ log.debug("2"); } }
8. Two objects, which lock the unified object, are classes
Although there are two objects, the lock is a static method, that is, the class itself. There is only one, and the effect returns to 1
@Slf4j public class EightLock_1 { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(()->{ log.debug("begin"); n1.a(); }, "t1").start(); new Thread(()->{ log.debug("begin"); n2.b(); }, "t2").start(); } } @Slf4j class Number{ //Member method, which locks the this object public static synchronized void a(){ //sleep will not release the lock after sleeping for one second sleep.mySleep(1); log.debug("1"); } //Member method that locks the this object public static synchronized void b(){ log.debug("2"); } }
9. Summary
In short, no matter how it changes, remember two concepts
- Member method lock object
- Static method lock class
Methods: those who have sleep look at the sleep time, and those who do not have sleep are randomized
7. synchronized knowledge supplement
This part is also extracted from the book "the art of java Concurrent Programming". Here we explain in detail what the mechanism of synchronized is
7.1 action mechanism
It mainly ensures that multiple threads can only have one thread in the method or synchronization block at the same time, so as to ensure the visibility and exclusivity of thread access to variables.
It is written in the book that the implementation of the synchronization block uses the monitorenter and monitorexit instructions to monitor and exit the synchronization block respectively. The synchronization method depends on the ACC above the method modifier_ Synchronized.
No matter which method is used, the essence is to obtain the monitor of an object. This process is exclusive, that is, only one thread can obtain the lock of the object protected by synchronized at the same time.
Any object has its own monitor. When the object is called by the synchronization block or synchronization method, the syn lock is the current object. At this time, the thread needs to obtain the monitor of the object before it can enter the synchronization block and synchronization method. What other threads do not obtain can only wait outside for blocking. Enter the BLOCKED state.
The relationship among object, monitor, synchronization queue and execution thread is shown in the figure below:
You can see the access of any thread to the Object object protected by synchronized. First try to obtain the monitor of the Object. After successful acquisition, you can enter the synchronization block or synchronization method for operation. When the acquisition fails, it will enter the synchronization queue and the thread state will become BLOCKED. When the precursor accessing the Object (the thread that obtained the lock) releases the lock, the thread BLOCKED in the synchronization queue will be awakened to try to obtain the lock again.
If there are any errors, please point them out