In multithreaded programming, the volatile
keyword is an important tool to ensure the visibility of variables between multiple threads and also helps prevent instruction reordering. Below, we will explain these concepts in detail and illustrate the usage scenarios and limitations of volatile
through practical examples.
Visibility
In a multithreaded environment, a modification of a shared variable by one thread may not be immediately visible to other threads. This is because a thread may cache the value of the variable instead of reading it directly from the main memory. For example:
class SharedObject {
private boolean flag = false;
public void setFlag() {
this.flag = true;
}
public boolean getFlag() {
return this.flag;
}
}
Suppose thread A
calls the setFlag()
method to set flag
to true
, while thread B
calls the getFlag()
method to check the value of flag
. Without using volatile
, thread B
may not see the modification of flag
by thread A
because the update of flag
may only exist in the cache of thread A
and has not been synchronized to the main memory.
How volatile
solves the visibility problem
By declaring a variable as volatile
, it ensures that all modifications to this variable are visible to all threads:
class SharedObject {
private volatile boolean flag = false;
public void setFlag() {
this.flag = true;
}
public boolean getFlag() {
return this.flag;
}
}
In this example, flag
is declared as volatile
, which means that every write operation to flag
will immediately update the main memory, and any thread reading the value of flag
will directly get the latest value from the main memory, thus ensuring the visibility of the variable.
Limitations of volatile
Although volatile
ensures visibility, it does not guarantee atomicity of operations. Atomicity means that an operation either succeeds completely or fails completely. For example, the count++
operation in the following code is not atomic:
class Counter {
private volatile int count = 0;
public void increment() {
count++;
}
}
The count++
operation actually consists of three steps:
- Read the value of
count
. - Increment the value.
- Write the value back to
count
.
If two threads execute the increment()
method simultaneously, they may read the same value of count
, then increment it separately, resulting in the value of count
being less than the actual number of increments.
How to ensure atomicity
To ensure atomicity of operations, synchronized
or the AtomicInteger
class can be used:
Using synchronized
:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
Using synchronized
ensures that only one thread can execute the increment()
method at a time, thus ensuring the atomicity of the count++
operation.
Using AtomicInteger
:
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
}
AtomicInteger
provides atomic operations such as incrementAndGet()
, which can be safely used for concurrent operations.
Instruction Reordering
Instruction reordering is the adjustment of the execution order of code instructions by the compiler and CPU to optimize performance. This may lead to unexpected behavior in a multithreaded environment. For example:
class Example {
private int x = 0;
private boolean flag = false;
public void method1() {
x = 1;
flag = true;
}
public void method2() {
if (flag) {
System.out.println(x);
}
}
}
Without using volatile
, the compiler or CPU may reorder the execution order of flag = true
and x = 1
, which may result in flag
being set to true
in method2
but x
not being updated yet.
How volatile
handles instruction reordering
The volatile
keyword prevents instruction reordering of volatile variables. After using the keyword, write operations will not be reordered before read operations, and read operations will not be reordered after write operations, thus avoiding the problems caused by instruction reordering:
class Example {
private volatile int x = 0;
private volatile boolean flag = false;
public void method1() {
x = 1;
flag = true;
}
public void method2() {
if (flag) {
System.out.println(x);
}
}
}
In this example, both flag
and x
are declared as volatile
, which ensures that flag = true
in method1
will not be reordered before x = 1
, so that x
can be correctly read in method2
.
In a nutshell
volatile
is an important tool in Java to ensure the visibility of variables among multiple threads and prevent instruction reordering. However, it does not guarantee atomicity of operations. In scenarios where atomicity needs to be ensured, consider using synchronized
or AtomicInteger
.