1、作用
volatile关键字在Java中主要用于保证内存可见性和禁止指令重排序
- 可见性:当一个线程修改了一个volatile变量,所有其他线程对该变量的读取都会看到最新的值。这是因为写操作会将变量的值写入主内存,而读操作会从主内存中获取最新值。
- 有序性:volatile还提供了一种称为“happens-before”的保证,这有助于维持代码的执行顺序,防止编译器和处理器为了优化而进行不必要的重排序。
2、使用volatile关键字解决多线程可见性的问题
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存
public class TestVolatile {
// private static volatile boolean stop = false;
private static boolean stop = false;
public static void main(String[] args) {
// Thread-A
new Thread("Thread A") {
@Override
public void run() {
while (!stop) {
}
System.out.println(Thread.currentThread() + " stopped");
}
}.start();
// Thread-main
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread() + " after 1 seconds");
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
}
}
当不使用volatile现象:
当不是用 volatile关键字修饰 stop变量时,当主线程 设置stop = false时,子线程A并不会停止,会一直运行下去
结论:
这是因为stop变量没有被volatile修饰,所以主线程修改stop变量的值后,子线程A无法感知到主线程修改stop变量的值,从而无法跳出循环,程序无法停止
当使用volatile运行结果:
当sopt=true时,子线程跳出循环,程序结束。
3、volatile只能保证多线程单次读写原子性
volatile可以保证变量的值会直接写入到主存中,对于其他线程来说,每次都是获取最新的值,因此可实现单次读写的原子性。但是对于i++这种看似一个操作,其实包含多次读写的操作,并不能保证其原子性,下面这个例子可以很好的证明:
public class VolatileTest01 {
volatile int i;
public void addI(){
i++;
}
public static void main(String[] args) throws InterruptedException {
final VolatileTest01 test01 = new VolatileTest01();
for (int n = 0; n < 1000; n++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test01.addI();
}
}).start();
}
Thread.sleep(10000);//等待10秒,保证上面程序执行完成
System.out.println(test01.i);
}
}
现象:
每次计算的的结果都小于1000
结论:
这是因为 volatile只能保证单次读或单次写的原子性 ,而 i++ 操作:会有三个步骤(并不是单次操作):
- 读取i的值
- 将i的值加1
- 将i的值写入主存 尽管volatile可以确保每次读写都是同步的,并且最新的值对所有线程都可见,但它无法保证整个i++操作作为一个整体不被中断。在多线程环境中,如果两个线程几乎同时尝试执行i++,它们都可能读取到相同的i值,各自独立地将其加1,然后写回新值。这样,i的值只会增加1,而不是预期的2。