The introduction
When it comes to the volatile keyword, most developers know something about it. It’s a keyword that developers are very familiar with, and then very unfamiliar with. Equivalent to lightweight synchronized, also known as lightweight lock, synchronized has less overhead than performance, but also has visibility, order and part of the atomicity, is a very important keyword in Java concurrency requirements. In this article, we will take an in-depth look at the underlying principles of volatile to ensure visibility, orderliness, and partial atomicity, as well as summarize some typical uses of the volatile keyword.
The “partial” atomicity of volatile
Atomicity means that an operation is a complete whole, and other threads can see that the operation has either not started or completed, without seeing the process in between, similar to a transaction.
The reason why volatile is only “partially” atomic is because volatile is not atomic at all. It modiifies only individual variables. In most cases, the reads and assignments of individual variables are atomic. Long /double operations on 32-bit Java virtual machines.
On a 32-bit Java virtual machine, long/double reads and writes are divided into two parts: high 32 bits and low 32 bits, or vice versa. This can lead to unpredictable results in multithreaded reads and writes if volatile variables are not declared. Since reading or writing a single long/double is not atomic, reading or writing a single long/double is atomic only with volatile. On a 64-bit Java virtual machine, long/double reads and writes are inherently atomic and do not need to use volatile for simple reads and writes.
It is important to note that volatile only guarantees that reads and writes of variables are atomic, not that composition of variables is atomic. The most classic scenario is the increment and decrement of individual variables.
private volatile static int increaseI = 0;
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run(a) {
increaseI++;
}
}, String.valueOf(i));
thread.start();
}
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(increaseI);
}
Copy the code
And if you test it, you’ll see that a lot of times, it’s not going to print 100,000. This is because volatile variables are only guaranteed to be atomic, while increaseI++ is a compound operation, which can be simply divided into:
var = increaseI; // Step 1: load the value of the increaseI register into the var
var = var + 1;// Step 2: Increment the value of register var by 1
increaseI = var;// Step 3: Write the value of register var to the increaseI
Copy the code
Volatile can only guarantee atomicity for the first and third individual operations, but not for the entire increment and decrement process, that is, volatile decorated increasee ++ is not an atomic operation. The following figure also illustrates the problem:
Visibility of volatile
As mentioned in Java Concurrency (2) — Happens-Before, shared variables are read and written in the thread’s local memory to improve operation efficiency. When variables are updated, the results of the variables are not flushed back to main memory. In a multi-threaded environment, Other threads will not read the latest variable value in time. We can analyze this from the following code.
private static boolean flag = false;
private static void refershFlag(a) throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
@Override
public void run(a) {
while(! flag) {//do something}}}); Thread threadB =new Thread(new Runnable() {
@Override
public void run(a) {
flag = true; }}); DateFormat dateFormat =new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
System.out.println("threadA start" + dateFormat.format(new java.util.Date()));
threadA.start();
Thread.sleep(100);
threadB.start();
threadA.join();
System.out.println("threadA end" + dateFormat.format(new java.util.Date()));
}
//threadA start2018/07/25 16:48:41
Copy the code
According to the normal logic for thread B variable flag, after A thread should quit right away, but in fact most of the time thread B does not exit immediately, this is because the virtual machine considering the Shared variables did not decorate using volatile, by default, this variable does not need multithreaded access and optimized, and lead to flag Shared variables not refresh in time back to the main memory, At the same time, other threads did not go to main memory in time to read the results. What if we made flag volatile?
private volatile static boolean flag = false;
//threadA start2018/07/25 16:48:59
//threadA end2018/07/25 16:48:59
Copy the code
You can see that thread A immediately exits, which shows the visibility of volatile.
The order of volatile
The JMM guarantees the ordering of single threads and properly synchronized multithreading based on the happens-before rule, which includes a volatile variable rule: writes to a volatile variable happen — before reads to that variable.
There are two things to note here. First, there is a happens-before relationship between writes and reads on the same volatile variable. Second, there is a chronological order in which writes happen — before reads. The nature of volatile prohibiting reordering is well illustrated in the reordering example Java Concurrency (2) – Happens-Before.
public class AAndB {
int x = 0;
int y = 0;
int a = 0;
int b = 0;
public void awrite(a) {
a = 1;
x = b;
}
public void bwrite(a) {
b = 1; y = a; }}public class AThread extends Thread{
private AAndB aAndB;
public AThread(AAndB aAndB) {
this.aAndB = aAndB;
}
@Override
public void run(a) {
super.run();
this.aAndB.awrite(); }}public class BThread extends Thread{
private AAndB aAndB;
public BThread(AAndB aAndB) {
this.aAndB = aAndB;
}
@Override
public void run(a) {
super.run();
this.aAndB.bwrite(); }}private static void testReSort(a) throws InterruptedException {
AAndB aAndB = new AAndB();
for (int i = 0; i < 10000; i++) {
AThread aThread = new AThread(aAndB);
BThread bThread = new BThread(aAndB);
aThread.start();
bThread.start();
aThread.join();
bThread.join();
if (aAndB.x == 0 && aAndB.y == 0) {
System.out.println("resort");
}
aAndB.x = aAndB.y = aAndB.a = aAndB.b = 0;
}
System.out.println("end");
}
Copy the code
Resort may be printed when both thread A and thread B reorder, but this is not the case when both variables are volatile.
Two typical use scenarios for volatile
1 indicates the status quantity. A status indicator is a Boolean variable that determines whether the logic needs to be executed. This is the code from volatile visibility above:
Thread threadA = new Thread(new Runnable() {
@Override
public void run(a) {
while(! flag) {//do something}}}); Thread threadB =new Thread(new Runnable() {
@Override
public void run(a) {
flag = true; }});Copy the code
This can be complicated with synchronized or locking, but using volatile to modify variables is a good way to solve this problem, ensuring that the state is flushed back to main memory and that updates are enforced by other threads.
The double-check problem is probably the most widely used scenario for volatile. The following code looks like this:
public class DoubleCheck {
private volatile static DoubleCheck instance = null;
private DoubleCheck(a) {}public static DoubleCheck getInstance(a) {
if (null == instance) { / / step one
synchronized (DoubleCheck.class) {
if (null == instance) { / / in step 2
instance = new DoubleCheck(); / / step 3}}}return instance;
}
public static void main(String[] args) throws InterruptedException { DoubleCheck doubleCheck = DoubleCheck.getInstance(); }}Copy the code
Step 3 in the code is not atomic, similar to the previous increment, which can be divided into three steps:
3.1 Alloc Memory Address allocated to DoubleCheck
3.2 Initializing the DoubleCheck Init DoubleCheck
3.3 Reference address to Instance Instance > Memory address
If 3.2 and 3.3 are not dependent on each other, it is possible to reorder them:
When thread 2 decides that instance is not empty at step 1, the object is not actually initialized, and 3.2 is not executed. Causes an error with the following object. Using volatile to modify instance variables prevents reordering in 3.2 and 3.3, thus ensuring that the code is correct when accessed by multiple threads.
We can see that the assembly code uses the volatile keyword and adds the lock instruction in step 3 to ensure that the current execution is orderly:
Using volatile
The rationale behind volatile
In the assembly code of DoubleCheck, we can see that the volatile keyword is added to the assembly code with a lock instruction. What does this instruction mean?
The lock directive has two functions:
- The CPU bus and cache are locked, subsequent instructions are executed, and data in the cache is flushed back to main memory when the lock is released.
- Lock invalidates cache rows in other CPUS ‘caches, which must load the latest data from main memory to read.
Simply put, the LOCK directive implements cache consistency. If the shared flag variable is volatile, each update of the flag value will force the cache row to be flushed to main memory, and the data previously volatile will be flushed back to main memory. At the same time, other threads must go to main memory to read the latest flag value. This enables visibility and order of shared variables.
- In-depth Understanding of the Java Virtual Machine
- The Art of Concurrent Programming in Java