一 、volatile关键字主要用法
1、变量声明
可以在变量声明时使用volatile关键字来修饰成员变量或局部变量(但局部变量使用场景较少)。
例如:
public class VolatileExample {
private volatile boolean flag = false;
private volatile int counter = 0;
}
2、多线程通信
1. 用于标志位
在多线程环境中,可以使用volatile变量作为标志位来控制线程的执行。例如,一个线程可以设置标志位为true来通知其他线程执行某个特定的操作。
代码示例:
public class ThreadTest03 {
private volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (flag) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
// 在主线程里就可以随时通过 flag 变量的取值, 来操作 t 线程是否结束.
flag = false;
}
}
2. 防止指令重排序优化导致的问题
在某些情况下,指令重排序可能会导致意外的结果。使用volatile可以防止特定的指令重排序,确保代码的执行顺序符合预期。
例如:在单例模式的双重检查锁实现中,使用volatile修饰单例对象的引用,以防止指令重排序导致另一个线程获取到未完全初始化的对象。 代码示例:
public class Singleton {
//volatile修饰instance,防止指令重排序导致另一个线程获取到未完全初始化的对象
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
二、volatile 关键字的作用
1. 保证可见性
当一个变量被声明为 volatile 时,对于多个线程对这个变量的读和写操作都是直接从内存中进行的,而不是从线程自己的缓存(寄存器)中读取。这样就确保了不同线程对该变量的修改能够立即被其他线程看到。
例如:在一个线程中修改了一个 volatile 变量的值,这个修改会立即刷新到内存中,并且其他线程在读取这个变量时,会从主内存中获取最新的值,而不是使用自己工作内存中的缓存值。
代码示例:
import java.util.Scanner;
class MyCounter {
volatile public int flag = 0;
}
public class ThreadDemo15 {
public static void main(String[] args) {
MyCounter myCounter = new MyCounter();
Thread t1 = new Thread(() -> {
while (myCounter.flag == 0) {
// 这个循环体咱们就空着
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 循环结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数: ");
myCounter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
t1 读的是自己工作内存中的内容. 当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化。如果给 flag 加上 volatile, 当 t2 对 flag 变量进行修改,t1立马就能感知到。
2. 禁止指令重排序
在 Java 中,为了提高性能,编译器和处理器可能会对指令进行重排序。但是,使用 volatile 关键字可以禁止特定的指令重排序。
例如:在某些情况下,编译器可能会对代码进行优化,将变量的初始化操作延迟到后面执行。但是,如果这个变量被声明为 volatile,那么编译器就不能进行重排序操作。
三、volatile 关键字的原理
1. 内存屏障
Java 中的 volatile 关键字是通过插入内存屏障来实现其作用的。它可以确保特定的操作在执行时不会被编译器或处理器重排序,并且可以保证对 volatile 变量的读写操作直接与主内存进行交互。
在对 volatile 变量进行写操作时,会在写操作之后插入一个 StoreStore 屏障,以确保前面的写操作对其他处理器可见,然后再插入一个 StoreLoad 屏障,以防止后面的读操作重排序到写操作之前。在对 volatile 变量进行读操作时,会在读操作之前插入一个 LoadLoad 屏障和一个 LoadStore 屏障,以确保读取到的是最新的值,并且防止前面的写操作重排序到读操作之后。
2. 缓存一致性协议
现代计算机系统通常使用缓存一致性协议来确保多个处理器之间的缓存数据的一致性。当一个处理器修改了一个 volatile 变量的值时,它会通过缓存一致性协议将这个修改通知其他处理器,其他处理器会使自己缓存中的该变量的值无效,并在下次使用该变量时从主内存中重新读取。这样就保证了不同处理器对 volatile 变量的可见性。
四、补充说明
内存屏障(Memory Barrier )是一种计算机硬件或软件机制,用于确保特定的内存操作在多线程或多处理器环境下的顺序和可见性。
保证顺序:在多线程或多处理器系统中,由于指令重排序的存在,编译器和处理器可能会对指令的执行顺序进行优化调整。内存屏障可以确保特定的内存操作,按照开发者的期望顺序执行,防止指令重排序影响程序的正确性。例如:
在一个线程中,对变量 A 的写操作必须在对变量 B 的写操作之前完成,插入一个内存屏障可以确保这种顺序在多线程环境下也能得到保证。
保证可见性:在多处理器系统中,每个处理器都有自己的缓存。内存屏障可以确保一个处理器对内存的修改,对其他处理器可见。例如:
一个线程在处理器 A 上修改了一个共享变量的值,插入一个内存屏障可以确保这个修改能够及时地被其他处理器看到,而不是一直停留在处理器 A 的缓存中。
缓存一致性协议是一种用于确保多个处理器或核心的缓存之间数据一致性的机制。在多处理器系统中,如果一个处理器修改了其缓存中的数据,其他处理器需要能够及时得知这个修改,以确保它们使用的是最新的数据 。
在 Java 中,volatile关键字的实现都依赖于内存屏障。当一个变量被声明为volatile时,编译器和处理器会在对这个变量的读写操作前后插入适当的内存屏障,以保证可见性和禁止指令重排序。
例如,在对一个volatile变量进行写操作时,Java 会在写操作之后插入一个写屏障,以确保前面的写操作对其他处理器可见,然后再插入一个读屏障,以防止后面的读操作重排序到写操作之前。这样就保证了对volatile变量的写操作能够被其他线程及时看到,并且不会被重排序到其他操作之后。