Volatile in Java
In Java, the volatile
keyword is used to mark a variable as "volatile", which indicates that its value may be modified by multiple threads simultaneously. The main purpose of volatile
is to ensure visibility and ordering of changes to a variable across threads.
Here’s an explanation of what volatile
does and when to use it:
1 Visibility Guarantee
When a variable is marked as volatile
, it guarantees that any changes to its value will be immediately visible to all threads. Without volatile
, the Java Memory Model (JMM) allows each thread to keep a local copy of variables, which can lead to stale values being read in a multi-threaded environment.
Example:
public class SharedData {
private volatile boolean flag = false;
public void changeFlag() {
flag = true; // This change will be visible to all threads
}
public boolean checkFlag() {
return flag; // Always returns the latest value of flag
}
}
In this example, if flag
was not declared volatile
, one thread could update flag
to true
, but other threads might still see it as false
due to caching.
2 Avoiding Caching and Reordering
The volatile
keyword prevents the JVM from caching the variable or reordering instructions related to the variable. Without volatile
, the compiler, processor, or runtime environment may reorder instructions for optimization, which can lead to subtle bugs in multi-threaded programs.
When a variable is declared volatile
, the following happens:
- Reads of the volatile variable always read from the main memory (instead of from thread-local cache).
- Writes to the volatile variable are always written to the main memory, making the change visible to other threads immediately.
- Reordering prevention: Any read or write before a volatile write or after a volatile read cannot be reordered by the JVM or processor.
Here’s an example where volatile
is used to avoid reording instructions:
/**
* A Singleton Class with Thread Safety
* Pros:
* 1. Thread safety is guaranteed
* 2. Client application can pass arguments
* 3. Lazy initialization achieved
* 4. Synchronization overhead is minimal and applicable only for first few threads when the variable is null
* Cons:
* 1. Extra if condition
*/
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return result;
}
}
It is necessary to use the volatile
keyword to modify uniqueInstance. The code uniqueInstance = new Singleton();
is actually executed in three steps:
- Allocate memory space for
uniqueInstance
. - Initialize
uniqueInstance
. - Point
uniqueInstance
to the allocated memory address.
However, due to the JVM’s instruction reordering feature, the execution order may become 1 -> 3 -> 2. Instruction reordering does not cause issues in a single-threaded environment, but in a multi-threaded environment, it can result in one thread obtaining an instance that hasn’t been initialized yet. For example, if thread T1 executes steps 1 and 3, and at that moment thread T2 calls getUniqueInstance()
, it will find that uniqueInstance
is not null and will return it, but uniqueInstance
has not yet been initialized.
NoteFor more information about Java Memory Model, please refer to this article Java Memory Model
3 When to Use volatile
- When multiple threads are reading and writing to a shared variable: Use
volatile
to ensure that changes made by one thread are visible to all other threads. - When you don’t need atomicity:
volatile
is useful for ensuring visibility but doesn’t guarantee atomicity (which means that complex operations like incrementing a variable are not thread-safe). For atomic operations, use synchronization or classes likeAtomicInteger
.
4 Volatile Does Not Ensure Atomicity
While volatile
guarantees visibility, it does not ensure that operations on the variable are atomic. For instance, the following increment operation is not thread-safe, even if count
is volatile:
private volatile int count;
public void increment() {
count++; // This is not thread-safe even though count is volatile
}
In this example, the increment (count++
) involves three operations: read, increment, and write. These operations are not atomic, so if multiple threads execute increment()
, the final value of count
could be incorrect due to race conditions. To ensure atomicity in this case, use synchronized
or AtomicInteger
or ReentrantLock
.
// synchronized
public synchronized void increase() {
inc++;
}
// AtomicInteger
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
// ReentrantLock
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally {
lock.unlock();
}
}
5 Volatile vs. Synchronization
volatile
provides a lightweight mechanism for ensuring visibility and ordering of changes to a variable across threads. It is faster than usingsynchronized
because it doesn’t block or provide mutual exclusion.synchronized
provides visibility and also guarantees atomicity by allowing only one thread at a time to execute a block of code. It also forces a happens-before relationship between threads.
6 Example of Correct Use of Volatile
Here’s an example where volatile
is sufficient:
class StopThread {
private volatile boolean running = true;
public void run() {
while (running) {
// Do some work
}
}
public void stop() {
running = false; // This change will be visible to the thread running the loop
}
}
In this example, volatile
is used to ensure that the running
variable is properly updated across multiple threads. One thread can safely stop the execution of the loop in another thread without needing synchronized
.
Summary of Volatile Characteristics:
- Visibility: Changes to a volatile variable are immediately visible to all threads.
- Prevents reordering: Memory operations involving volatile variables are not reordered.
- No atomicity: Volatile does not make compound actions (like incrementing) atomic.
- Faster than
synchronized
: Volatile is less expensive than locking mechanisms but does not provide mutual exclusion.
In summary, the volatile
keyword is useful for ensuring visibility and ordering in concurrent programming but should be used carefully and only when synchronization is not required.