Make writing a habit together! This is the fifth day of my participation in the “Gold Digging Day New Plan · April More text Challenge”. Click here for more details.
@[TOC] learned about the use of synchronized and the basic components of the Java memory model JMM in the previous article. So now, let’s look at a new problem.
1. Visibility issues
Let’s look at the following code:
package com.dhb.concurrent.test;
import java.util.concurrent.TimeUnit;
public class VolatileTest {
private static int INIT = 0;
private static int MAX = 10;
public static void main(String[] args) {
new Thread(() -> {
int local = INIT;
while (local < MAX) {
if (local != INIT) {
System.out.println("Get change for INIT [" + INIT + "] local is [" + local + "]");
local = INIT;
}
}
}, "Reader Thread").start();
new Thread(() -> {
int local = INIT;
while (local < MAX) {
System.out.println("Update value [" + local + "] to [" + (local++) + "]");
INIT = local;
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Writer Thread").start();
}
}
Copy the code
There are two threads, one thread writes and the other thread reads, so can the result of the writing thread be read by the reader thread every time? We execute the code above:
Update value [0] to [0]
Get change for INIT [1] local is [0]
Update value [1] to [1]
Update value [2] to [2]
Update value [3] to [3]
Update value [4] to [4]
Update value [5] to [5]
Update value [6] to [6]
Update value [7] to [7]
Update value [8] to [8]
Update value [9] to [9]
Copy the code
As you can see, while the writer thread increments the INIT result, the reader thread reads only the first value. No matter how the writer thread then changes the value of INIT, in the reader thread, that value is still the result of the previous value, the value added by the writer thread, and is virtually invisible to the reader thread. That’s what we’re going to talk about today, visibility.
As shown above, each thread is actually a copy of a variable that it operates on in its own working memory, and if that copy is modified, it is synchronized back to main memory. So Writer writes the result to INIT in main memory each time. For the Reader thread, however, the value of 1 is read only once after it has been read for comparison, and the value is not changed after that, but is kept in the thread’s working memory. This is where the visibility problem comes in. In effect, since the Reader thread only reads and uses the use operation internally, it does not assign, so there is no need to load the variable from main memory every time. This is intended to increase the efficiency of JVM memory calculations, since the actual portion of working memory may be computed in the CPU’s cache. If the working memory is loaded from main memory each time, the speed difference between the cache and main memory can cause unnecessary system overhead. So how do we solve this problem? This is the key keyword of this article, volatile. This keyword has two functions:
- Keep memory visible
- Static instruction reordering
Let’s start with memory visibility. How does volatile keep memory visible? Simple. In fact, if a variable is volatile, the working memory cache is no longer used, but the main memory is loaded each time. In this way, although there is some performance loss, it can better solve the system consistency.
As shown in the figure above, the Reader thread’s working memory synchronizes its INIT each time it reads it from main memory. Changes made to main memory by the Writer thread are thus visible to the Reader thread. We modify the code as follows:
public class VolatileTest {
private volatile static int INIT = 0;
private static int MAX = 10;
public static void main(String[] args) {
new Thread(() -> {
int local = INIT;
while (local < MAX) {
if (local != INIT) {
System.out.println("Get change for INIT [" + INIT + "] local is [" + local + "]");
local = INIT;
}
}
}, "Reader Thread").start();
new Thread(() -> {
int local = INIT;
while (local < MAX) {
System.out.println("Update value [" + local + "] to [" + (local++) + "]");
INIT = local;
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Writer Thread").start();
}
}
Copy the code
Execution Result:
Update value [0] to [0]
Get change for INIT [1] local is [0]
Update value [1] to [1]
Get change for INIT [2] local is [1]
Update value [2] to [2]
Get change for INIT [3] local is [2]
Update value [3] to [3]
Get change for INIT [4] local is [3]
Update value [4] to [4]
Get change for INIT [5] local is [4]
Update value [5] to [5]
Get change for INIT [6] local is [5]
Update value [6] to [6]
Get change for INIT [7] local is [6]
Update value [7] to [7]
Get change for INIT [8] local is [7]
Update value [8] to [8]
Get change for INIT [9] local is [8]
Update value [9] to [9]
Get change for INIT [10] local is [9]
Copy the code
So the reader thread can read the latest INIT value every time.
2. Command reorder
Let’s look at this example:
package com.dhb.concurrent.test; public class VolatileTest2 { static int a,b,x,y; public static void main(String[] args) { long time = 0; while (true) { time ++; a = 0; b = 0; x = 0; y = 0; Thread t1 = new Thread(() -> { a = 1; x = b; }); Thread t2 = new Thread(() -> { b = 1; y = a; }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } if( x==0 && y==0) { break; } } System.out.println("time:["+time+"] x:["+x+"] y:["+y+"]"); }}Copy the code
For instruction reordering, if two statements in a thread have no relationship to each other, then when instructions are optimized within the JVM, statements written later can be optimized to be executed earlier. In this example, a=1, x=b are two unrelated statements, as is b=1, y=a in another thread. These statements are not sequential with each other. In traditional logic, the combination of the two, no matter which line is executed first or at the same time, will not result in x=0,y=0.
The implementation of | The results of |
---|---|
T1 is executed before T2 | x=0,y=1 |
T2 is executed before T1 | x=1,y=0 |
T1 and T2 are executed simultaneously | x=1,y=1 |
X =0,y=0 is possible only if instructions are reordered so that x=a and y=b are executed before a=1 and b=1. | |
We execute the code above: | |
` ` ` | |
time:[102946] x:[0] y:[0] | |
` ` ` | |
This instruction reorder is not a necessary event, so the result of this code is different each time it is executed: | |
` ` ` | |
time:[8943] x:[0] y:[0] | |
` ` ` | |
With luck, that could happen soon. | |
This is the instruction reorder problem. Another effect of volatile, then, is to prevent instruction reordering, which would not occur. This is also a common interview question, and why the DCL singleton pattern needs to be volatile. |
3. Synchronized and visibility
In the JMM, there are two rules about synchronized:
- The thread must flush the latest value of the shared variable to main memory before it can be unlocked.
- When a thread locks, it empties the value of the shared variable in working memory so that the shared variable needs to be fetched from main memory. (Locking and unlocking are the same lock)
Synchronized, therefore, can actually achieve visibility. Synchronized also uses synchronized locks and is atomic. Returning to the previous example, we reconfigure the program as follows:
package com.dhb.concurrent.test; import java.util.concurrent.TimeUnit; public class SyncTest { private static Count count = new Count(); private static int MAX = 10; public static void main(String[] args) { new Thread(() -> { int local = count.getCount(); while (local < MAX) { synchronized (count) { if (local ! = count.getCount()) { System.out.println("Get change for INIT [" + count.getCount() + "] local is [" + local + "]"); local = count.getCount(); } } } }, "Reader Thread").start(); new Thread(() -> { int local = count.getCount(); while (local < MAX) { synchronized (count) { System.out.println("Update value [" + local + "] to [" + (++local) + "]"); count.setCount(local); } try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Writer Thread").start(); } private static class Count { int count = 0; public int add() { return ++count; } public int getCount() { return count; } public void setCount(int count) { this.count = count; }}}Copy the code
Then we will look at the execution result:
Update value [0] to [1]
Get change for INIT [1] local is [0]
Update value [1] to [2]
Get change for INIT [2] local is [1]
Update value [2] to [3]
Get change for INIT [3] local is [2]
Update value [3] to [4]
Get change for INIT [4] local is [3]
Update value [4] to [5]
Get change for INIT [5] local is [4]
Update value [5] to [6]
Get change for INIT [6] local is [5]
Update value [6] to [7]
Get change for INIT [7] local is [6]
Update value [7] to [8]
Get change for INIT [8] local is [7]
Update value [8] to [9]
Get change for INIT [9] local is [8]
Update value [9] to [10]
Get change for INIT [10] local is [9]
Copy the code
This is also a good way to solve the visibility problem with synchronized. Since synchronized has been optimized in 1.8 for performance comparable to that of ReentrantLock, it is possible to achieve visibility with synchronized as long as instruction reordering is not involved.
As shown in the figure above, after locking, synchronization needs to be refreshed from main memory every time a read or write is performed.
4. Happens-before rules
In understanding the Java Virtual Machine, the happens-before principle is summarized as follows:
- 1. Program order rule: within the same thread, according to the code order, the operation written in the first place of the associated code takes precedence over the operation written in the second place.
- 2. Pipe lock rule: An UNLOCK operation takes precedence over subsequent lock operations on the same lock.
- 3. Volatile variable rules: Writes to a variable take precedence over subsequent reads of that variable. (Chronological order)
- 4. Thread start rule: The Thread start method takes precedence over every action of the Thread.
- 5. Thread termination rule: All operations in a Thread occur first in Thread termination detection, which can be detected by means of thread.join (), thread.isalive () return value, etc.
- 6. Thread interrupt rule: Calls to the threadinterrupt () method occur before code in the interrupted thread detects the occurrence of an event.
- 7. Object finalization rule: An object is initialized before the finalize() method that happens.
- 8. Transitivity rule: If operation A is visible to operation B, and operation B is visible to operation C, then operation A is also visible to operation C.
How to understand these happens-before rules? Happens-before does not mean that the previous action takes place Before the subsequent action. Rather, it means that the result of a previous operation is visible to subsequent operations. In effect, happens-before constrons the compiler’s behavior. The compiler can optimize according to the order in which multiple lines of code need to be executed, but such optimizations by the compiler must follow the happens-before rule.
5. Understanding happens-before
5.1 Program order rules
It is important to note that this rule does not apply when a JVM is optimized to write code that does not have any dependencies between them. The following code:
int a = 3; Int b = a + 1; / / code 2Copy the code
Because of dependencies, the result of code 1 is always visible to code 2.
int a = 3; Int b = 2; / / code 2Copy the code
The above code, then, has no order relationship, and JVM instructions are optimized as needed. The order after optimization is not necessarily the same. It could be code 1, or it could be code 2.
5.2 Pipe lock rules
This rule is easy to understand. It means that when a lock is unlocked, the unlock operation must be performed first.
5.3 Volatile Variable Rules
If a variable is volatile, writes to that variable must be visible to all subsequent reads. This is also demonstrated by the previous examples in this article.
public class VolatileTest {
private volatile static int INIT = 0;
private static int MAX = 10;
public static void main(String[] args) {
new Thread(() -> {
int local = INIT;
while (local < MAX) {
if (local != INIT) {
System.out.println("Get change for INIT [" + INIT + "] local is [" + local + "]");
local = INIT;
}
}
}, "Reader Thread").start();
new Thread(() -> {
int local = INIT;
while (local < MAX) {
System.out.println("Update value [" + local + "] to [" + (local++) + "]");
INIT = local;
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Writer Thread").start();
}
}
Copy the code
5.4 Transitivity Rules
If operation A precedes operation B and operation B precedes operation C, it follows that operation A precedes operation C. It’s kind of like the transitivity rule in mathematics. If A>B,B>C, then A>C.
package com.dhb.concurrent.test; public class VolatileExample { static int x = 0; static volatile boolean v = false; public static void main(String[] args) { Thread t1 = new Thread(() -> { x = 42; v = true; }); Thread t2 = new Thread(() -> { if(v == true){ System.out.println(x); }else { System.out.println(x); }}); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); }}}Copy the code
In the example above, if T1 must be executed first, the result output must be 42 according to transitivity rules.