volatile
是Java语言提供的最轻量级的同步机制。了解volatile
的关键点包括理解其对可见性和有序性的影响,以及理解它何时以及如何使用。
volatile的作用
-
保证可见性
当一个字段被声明为
volatile
,那么线程将直接从主内存读取该字段的值,即使缓存中已经存在该字段的副本。同时,当线程写入volatile
变量时,修改后的值会立即被刷新回主内存中,这确保了不同线程对这个变量的读写操作都能获得最新的值。 -
防止指令重排
在Java内存模型中,编译器和处理器可能会对指令进行重排,以提高程序的性能。
volatile
变量的读写操作不会被编译器和处理器重排到其他内存操作之前或之后,即保持了操作的有序性。
volatile的使用限制
volatile
不能保证原子性,比如volatile int i = 0; i++;
这个操作实际上是三个独立的操作(读取、增加、写入),并不是一个原子操作。volatile
适用于一个线程写入而其他线程读取的场景。如果多个线程对一个volatile
变量进行写入,那么你仍然需要使用其他同步机制来保证只有一个线程能够写入。
源码分析
在Java中,volatile
关键字的行为是由Java虚拟机底层实现的,因此它的功能不是通过源码实现的,而是通过JVM在运行时提供内存屏障来实现。
以下是一个简单的Java类,演示了如何使用volatile
关键字和其效果:
java
public class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写操作,立即刷新到主内存
}
public void reader() {
if (flag) { // 读操作,直接从主内存读取
// do something when flag is true
}
}
}
在这个例子中,flag
变量被声明为volatile
。如果一个线程调用writer
方法并写入flag
,那么这个值会立即写入主内存中。如果另一个线程调用reader
方法,它会直接从主内存中读取flag
的最新值,而不是从CPU缓存中读取。
Java内存屏障
虽然不能直接查看到volatile
关键字在源码层面的实现,但可以知道Java在编译时会插入特定的内存屏障指令来实现volatile
的语义:
-
读取屏障(Load Barrier)
在读取
volatile
变量后插入,保证之后的读写操作可以看到这个volatile
变量的最新值。 -
写入屏障(Store Barrier)
在写入
volatile
变量前插入,确保在这个volatile
变量之前的所有操作都已经完成,并且结果对其他线程可见。
代码演示
假设有两个线程,一个负责写入,一个负责读取:
java
public class VolatileDemo {
private volatile boolean ready = false;
private int number;
private class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield(); // 让出CPU时间,等待ready变为true
}
System.out.println(number);
}
}
private class WriterThread extends Thread {
@Override
public void run() {
number = 42; // 写操作
ready = true; // 写操作
}
}
public void demo() {
new ReaderThread().start();
new WriterThread().start();
}
public static void main(String[] args) {
new VolatileDemo().demo();
}
}
在这段代码中,volatile
关键字确保:
ready
的值对所有线程立即可见。number
的写入操作在ready = true;
之前完成,避免了指令重排。
总结
volatile
关键字为Java提供了一种确保可见性和有序性,但不保证原子性的同步机制。理解volatile
的限制和合适的使用场景对于编写正确的并发代码至关重要。在正确的场景下使用volatile
可以提供一种比synchronized
更轻量级的同步选项,但它的使用必须非常谨慎。