“This is the fifth day of my participation in the First Challenge 2022. For details: First Challenge 2022”
preface
- We learned about the memory model of objects and the relationship between biased, lightweight, and heavyweight locks in Java. These are low-level Java knowledge, today we will talk about our common multi-threaded development of the two keywords, or source preferred code keywords
volatile
Volatile ensures memory visibility and prevents instruction reordering
Memory visibility
- This is where the keyword volatile comes in. But volatile is not a complete solution to the problem of variable inconsistencies in multiple threads. We also need to look at this keyword dialectically.
- about
volatile
To understand this, we need to understand the JVM’s strategy for allocating memory. When it comes to JVMS, I’ve always wanted to do this chapter, but I don’t have time to learn about JVMS in detail. Let’s take a quick look at how the JVM stack is distributed to threads
- Object memory distribution model we know. But where memory is allocated is controlled by the JVM. Objects in the JVM are, unsurprisingly, stored in the heap (not to mention stack allocation and memory escape techniques). So what’s in the stack? The reference in the stack that stores our variable points to the center. There is also the concept of a virtual machine stack in the stack. Create a space for each thread in the virtual machine stack. The operations of each thread are performed in its own space. This provides isolation between threads. It is because of this isolation that our variables are out of sync. In the figure above, we can also see that each thread copies data from main memory (heap) to its own space the first time it uses a variable, and all subsequent operations on the variable are for its own internal variables. It writes the latest contents back to main memory (heap) after operating on its own internal variables.
- Since main memory data is synchronized only for the first time, thread A’s modification of object M after thread B’s synchronization will not be affected. But if we modify before thread B synchronizes, that’s fine.
public class Volatile {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run(a) {
while (flag) {
}
System.out.println("The flag has been changed, so I have to go too..."); }},"hellone").start();
Thread.sleep(1000);
flag=false;
System.out.println("I'm the main thread"); }}Copy the code
-
The above code consists of two threads, the main Thread and the hellone Thread, focusing on thread.sleep (1000). Without this code, there is a high probability that the main thread will execute first, because it takes time for the thread hellone to start. Therefore, if there is no sleep in the code, we will see the effect of the normal end, and first I am the main thread after the output flag has been changed, I also need to go…
-
But when we add sleep, the program never stops. Since flag is always true in thread Hellone, no matter how the main thread is modified, the Hellone thread will never synchronize flag 🈯️ from main memory again.
-
The solution to this code is to introduce the volatile keyword. Because one of its functions is memory visibility. Adding volatile to flag ensures that each time the variable is used in a thread, a copy is synchronized from main memory to the thread in the current vm stack.
public class Volatile {
static volatile boolean flag = true;
static int index=0;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run(a) {
while (flag) {
index++;
}
System.out.println("The flag has been changed, so I have to go too..."+index); }},"hellone").start();
Thread.sleep(1000);
flag=false;
System.out.println("I'm the main thread"); }}Copy the code
- The final result is as follows
- The reason for adding index is so you can see how many times the Hellone thread loops. Volatile simply ensures thread visibility. However, the thread is not guaranteed to be completely safe, as it also takes time to synchronize data from main memory. Nor is index a complete indication that volatile does not guarantee thread safety. We’re looking at the following code
public class VolatileAddMoreThread {
static volatile Integer index = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
index = 0;
int m = 100;
final int n = 100;
List<Thread> threadList = new ArrayList<Thread>();
for (int i = 0; i < m; i++) {
Thread thread = new Thread(new Runnable() {
public void run(a) {
for (int i1 = 0; i1 < n; i1++) { index++; }}}); thread.start(); threadList.add(thread); }for (Thread thread : threadList) {
thread.join();
}
System.out.println(index);
if (index < m * n) {
System.out.println("Thread unsafe index="+index);
break; }}}}Copy the code
- Here’s a quick look at the code above. The class has an integer variable that is volatile. It then increments the integers continuously by 100 threads. If volatile is thread safe theoretically index=100*100=10000; In fact, no matter how it works, it’s rarely 10000. Here will not map, interested in their own copy down to perform it depends on the situation.
- I just want to show that volatie guarantees visibility, but there is a classic problem with multi-threading, when multiple threads read the same thing at the same time and increment it. This results in the final index<=10000.
- Why is that? Again, it’s mainly because index++ is non-atomic. Actually in assembly i++ is executed by multiple assembly instructions. The main memory is switched by the CPU when executing part of the instruction is not written. At this point, the value has actually changed but main memory has not had time to do so, causing other threads to read the old value and causing index<=10000
Disallow instruction reordering
- We learned above that volatile provides memory visibility, which allows us to achieve final consistency of variables across threads. But there’s still a thread-unsafe problem, and there’s a convenience problem because we’re doing non-atomic operations in threads.
- Now that we have concluded that volatile does not guarantee thread safety, let’s look at another feature of volatile
Disallow instruction reordering
public class InstructionReorder {
static int a;
static int b;
static int x;
static int y;
public static void main(String[] args) throws InterruptedException {
int index=0;
while (true) {
index++;
a=0; b=0; y=0; x=0;
final Thread thread = new Thread(new Runnable() {
public void run(a) {
a = 1; x = b; }});final Thread thread1 = new Thread(new Runnable() {
public void run(a) {
b = 1; y = a; }}); thread.start(); thread1.start(); thread.join(); thread1.join();if (x == 0 && y == 0) {
System.out.println("index="+index+"x,y="+x+y);
break; }}}}Copy the code
- Before running this code, LET me analyze its intent. First, there is an endless loop that creates two threads, one for a, one for b, one for x, and one for y. The logic is very simple and we’re looking at what the values of x and y are in each loop.
- We also want to emphasize here that multithreaded execution is not all the way to the end, because the CPU is so fast that it will switch back and forth. Therefore, if thread execution reaches a=1, the CPU will switch to thread1 to execute the logic b=1, so we can predict the situation at x and y after permutation and combination
x | y |
---|---|
1 | 1 |
0 | 1 |
1 | 0 |
- It is impossible for both x and y to be 0, because at least one of a and B will be assigned before x and y are assigned, so our code will evaluate both x and y at the end of each loop. The above code will always execute unless x and y are both 0. You estimate the time based on your computer’s ability. Trust me if I keep the program running it will end in the middle.
- It’s the number of times that I calculated it, 253,449 times and finally it’s the case that x and y are both 0, but w, H, and y are theoretically impossible. This brings us to the subject of this chapter, instruction reordering
- Interested students can add the volatile modifier before a,b,x, and y. Let’s run the program and see if it stops halfway through. I tested here that it stopped in 10 minutes at most without volatile, and continued running from 12 noon to 12 noon the next day with volatile modifications. I don’t think I need to say more. We can conclude that volatile disallows instruction reordering.
- So why is instruction reordering happening in Java? What is instruction reorder? What does reordering do?
- I mentioned earlier that it’s impossible to run from start to finish in a multithreaded situation, it’s bound to be scheduled by the CPU and executed by another thread. The constant switching of threads raises the question of how the CPU knows where thread A is going when thread A switches to thread B in the middle of its execution. And CPU switch from time to time arbitrary switch?
- First, although CPU is task scheduling, it is not arbitrary. First, Java is finally assembly instructions. The CPU must also wait for the thread to complete an instruction before switching threads. So instructions are holistic. Instructions are either executed or not executed; there is no half-execution. Then you have a program counter module in the JVM. This module records the number of lines of instruction that each thread executes. Wait for the CPU to switch over to continue the execution of the previous instruction
- We can easily see that there are multiple instructions in 17. And there is no absolute correlation between a=1 and x=b, so it is possible for x= B to be executed first. So you’re going to have a situation where both x and y are 0.
- Here we have examined the effects of volatile in two different cases. Memory visibility ensures final consistency of thread data; Disallow instruction reordering to avoid data confusion.
synchronized
public class SimpleTest { public static void main(String[] args) { synchronized (SimpleTest.class) { System.out.println("hello world"); }}}Copy the code
- It’s a very simple one up here
synchronized
The use of. Let’s look at the corresponding bytecode section
synchronized
In fact, the bytecode layer isMONITORENTER
andMONITOREXIT
; It is worth noting that before the MONITOR instruction we had an instruction on the operation stack. That’s what we aresynchronized
What follows the parentheses
- If we change it a little bit, we can see that the object is different. We can understand the concept of locking from a stack perspective.
- Concurrency is often mentioned
synchronized
The keyword is persistent, and we overcame the keyword volatile above. That leaves synchronized. First let’s look at the concept synchronized
The function is to lock, in multithreaded development bysynchronized
Decorated code is owned by only one thread at a time, and decorated code cannot be broken. Serial execution in multithreading is guaranteed. We can think of it as atomicity- In addition
synchronized
andLock
We’re not going to do the expansion here. To summarize, the former is a Java language keyword, the latter is a Java class; In addition, the former native lock the latter need to unlock and unlock themselves. synchronized
The objects decorated by keywords can be in one of the following situations
Modify the object | role |
---|---|
The code block | The code block being decorated is a synchronized code block. The scope of the code block lock depends on the modifier object |
Common methods | Synchronizing a method locks the object on which the method is called |
A static method | Synchronizing a method locks the Class object on which the method is called |
decorator | Lock the Class object |
Modify the object | Lock Java objects |
- The summary above is quite abstract. Actually,
synchronized
There are only two cases where the scope of the lock is different - One is to lock the Class object. One is to lock Java objects.
trailer
- We will continue to address the locking object problem, the locking class problem, and the volatile keyword in single-column mode