The Ultimate Guide to Using volatile in Java
You are probably familiar with the volatile keyword in Java. Before JDK 1.4, volatile semantics could only ensure visibility and not ordering. However, after the JDK 1.5 version, with the implementation of the JSR-133 Java Memory Model specification, the volatile semantics were enhanced to support both visibility and ordering (preventing instruction reordering). This article will introduce the volatile semantics, use cases, and best practices.
Introduction to volatile
In Java, volatile is a keyword used to ensure memory visibility and prevent reordering issues in multi-threaded applications. When a variable is declared as volatile, every time a Java thread reads the variable, it will read its most recently written value. This means it can be used to ensure that all threads see a consistent value of a shared variable.
The volatile keyword is primarily used in the following two scenarios:
- Memory Visibility: Ensures that all threads see a consistent value of a shared variable.
- Preventing Instruction Reordering: Ensures that the execution order of the code is not different from the expected due to compiler optimizations.
volatile Semantics
After the JDK 1.5 version, volatile can ensure visibility and ordering.
- Visibility: When a thread modifies the value of a volatile-modified variable, the new value will be immediately visible to other threads. This is because when writing to a volatile variable, the modified value is forced to be immediately flushed to the main memory, allowing other threads to see the latest value. Similarly, when a thread reads a volatile-modified variable, it will get the latest value from the main memory instead of using the thread’s local cache.
- Ordering (Preventing Instruction Reordering): Read and write operations on variables modified by volatile will be inserted with memory barriers, preventing instruction reordering. Instruction reordering is an optimization technique used by compilers and processors to optimize program execution by changing the order of instructions, but it can sometimes cause issues in multi-threaded environments. By using the volatile keyword, you can ensure that operations before writing to a volatile variable are not reordered after the write, and operations after reading a volatile variable are not reordered before the read.
Use Cases for volatile
Since volatile cannot guarantee atomicity, it cannot be used for non-atomic operations, such as i++. Suitable scenarios:
- Lightweight Synchronization Requirements: The volatile keyword provides a lightweight synchronization mechanism suitable for simple concurrent scenarios, such as using a volatile variable for a simple counter operation.
- Flag Scenarios: If a shared variable is only assigned values by various threads without any other operations, volatile can be used instead of synchronized or atomic variables. This is because the assignment itself is atomic, and volatile ensures visibility, which is sufficient to guarantee thread safety. Here’s a simple demo:
public class VolatileDemo {
private volatile boolean flag = false;
public void start() {
new Thread(() -> {
System.out.println("Thread 1: Waiting for flag to be true...");
while (!flag) {
}
System.out.println("Thread 1: Flag is now true. Exiting...");
}).start();
new Thread(() -> {
System.out.println("Thread 2: Sleeping for 2 seconds...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Setting flag to true...");
flag = true;
}).start();
}
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
demo.start();
}
}
- As a Trigger to Refresh Previous Variables: Using volatile int x ensures that after reading x, all previous variables are visible. This is mainly implemented based on the happens-before principle. Here’s a demo:
package com.bearboy.thread.jmm;
import java.util.concurrent.TimeUnit;
public class VolatileTriggerDemo {
private volatile int x = 0;
private boolean flag = false;
public void writerThread() {
try {
Thread.sleep(1000);
flag = true;
x = 1; // Modify the value of x
System.out.println("set x=1 end....");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void readerThread() {
int j = 0;
while (!flag) {
j = x;
System.out.println("j = " +j);
}
System.out.println("end Value of x: " + x);
}
public static void main(String[] args) {
VolatileTriggerDemo demo = new VolatileTriggerDemo();
Thread writerThread = new Thread(demo::writerThread);
Thread readerThread = new Thread(demo::readerThread);
writerThread.start();
readerThread.start();
}
}
Best Practices for volatile
- Visibility Guarantee: The volatile keyword can ensure that write operations on a variable are visible to other threads, so it should be used for variables that need to share state across multiple threads. This can avoid data inconsistency issues between threads.
- Avoid Compound Operations: The volatile keyword cannot guarantee the atomicity of compound operations. If you need to perform compound operations (e.g., increment or decrement), consider using atomic classes (such as AtomicInteger) or other synchronization mechanisms (such as synchronized or Lock) to ensure atomicity.
- Avoid Overuse: The volatile keyword adds some overhead, so you should avoid overusing it. Only use volatile when you need to ensure visibility, not for all variables.
- Avoid Relying on Atomicity: The volatile keyword cannot guarantee atomicity, so you should not rely on volatile in situations where atomic operations are required. Instead, use atomic classes (such as AtomicInteger, AtomicLong, etc.) or other synchronization mechanisms to ensure atomicity.
- Use with Other Synchronization Mechanisms: The volatile keyword is suitable for simple synchronization requirements, but for more complex synchronization needs, it should be used in conjunction with other synchronization mechanisms, such as the synchronized keyword or the Lock interface, to provide stronger thread safety guarantees.
- Understand Memory Semantics: When using the volatile keyword, you need to correctly understand the memory semantics it provides. The volatile keyword ensures visibility and prevents instruction reordering, but it does not provide atomicity.
- Consider Using Concurrent Collection Classes: Java provides many concurrent collection classes, such as ConcurrentHashMap, ConcurrentLinkedQueue, etc., which internally use appropriate synchronization mechanisms to ensure thread safety. In multi-threaded environments, you should prioritize using these concurrent collection classes rather than manually using volatile.
In summary, the volatile keyword is a lightweight synchronization mechanism suitable for simple concurrent scenarios. You need to be aware of its usage limitations and memory semantics and combine it with other synchronization mechanisms to provide stronger thread safety guarantees. When designing and implementing multi-threaded applications, you should comprehensively consider the suitability of using volatile and other synchronization mechanisms based on your specific requirements.