博主介绍: ✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,阿里云专家博主,华为云云享专家✌
💕💕 感兴趣的同学可以收藏关注下 ,不然下次找不到哟💕💕
1、什么是 volatile
volatile是一个关键字,用于修饰变量。在Java中,volatile关键字用于确保多线程环境下的可见性和禁止指令重排序。
当一个变量被声明为volatile时,它的值的修改将立即对其他线程可见,而不会被线程的本地缓存所影响。这样可以确保多个线程对该变量的读写操作是一致的。
此外,volatile关键字还可以禁止编译器和处理器对指令的重排序优化,保证指令的执行顺序与程序的顺序一致。
需要注意的是,volatile关键字只能保证可见性和禁止指令重排序,并不能保证原子性。如果需要保证原子性操作,可以考虑使用synchronized关键字或者Atomic类。
2、volatile 能解决什么问题
volatile关键字主要用于解决多线程环境下的可见性和禁止指令重排序的问题。下面分别说明这两个问题:
可见性问题:在多线程环境下,当一个线程修改了一个共享变量的值时,其他线程可能无法立即看到这个修改,而是看到该变量的旧值。这是因为每个线程都有自己的本地缓存,对共享变量的修改可能先保存在本地缓存中,而不是立即写回主内存。这种情况下,其他线程就无法感知到这个修改,导致数据不一致的问题。使用volatile关键字修饰的变量,其修改操作将立即对其他线程可见,不会被本地缓存所影响,从而解决了可见性问题。
指令重排序问题:为了提高程序的执行效率,编译器和处理器在执行指令时可能会对其进行重排序优化。这种重排序优化在单线程环境下不会影响程序的结果,但在多线程环境下可能会导致线程之间的执行顺序出现问题,从而破坏了程序的正确性。使用volatile关键字修饰的变量,可以禁止编译器和处理器对指令的重排序优化,保证指令的执行顺序与程序的顺序一致,从而解决了指令重排序问题。
3、volatile 的底层原理
volatile的底层原理涉及到Java内存模型(Java Memory Model,简称JMM)和CPU的缓存机制。
在Java内存模型中,每个线程都有自己的工作内存,而共享变量则存储在主内存中。当一个线程要访问共享变量时,首先会将该变量从主内存中拷贝到自己的工作内存中,然后对该变量进行操作。操作完成后,再将变量的值写回到主内存中。
而对于被volatile修饰的变量,其读取和写入操作会有特殊的规则。当一个线程对volatile变量进行写操作时,会立即将修改后的值刷新到主内存中,而不是先在自己的工作内存中进行修改。这样,其他线程在读取该变量时就可以立即看到最新的值。同样地,当一个线程对volatile变量进行读操作时,会先将该变量的值从主内存中读取到自己的工作内存中,而不是从自己的本地缓存中读取。
底层实现上,volatile关键字通过使用内存屏障(Memory Barrier)来实现可见性和禁止指令重排序。内存屏障是一种硬件指令,它可以保证在执行屏障之前的所有读写操作都完成,并将结果刷新到主内存中,然后再执行屏障之后的读写操作。这样可以确保volatile变量的读写操作不会受到指令重排序的影响,从而保证了可见性和一致性。
4、指令重排(面试必问)
指令重排是指在编译器和处理器优化指令执行顺序的过程中,可能会改变原始程序中的指令顺序。这种优化是为了提高程序的执行效率和性能。然而,在多线程环境下,指令重排可能会导致线程之间的执行顺序出现问题,从而破坏了程序的正确性。
指令重排的原因是现代计算机体系结构中存在多级缓存和乱序执行等机制。处理器为了提高指令执行的效率,可能会对指令进行重排序,但要保证最终的执行结果与原始程序的顺序一致。
在多线程环境下,指令重排可能会引发一些问题,例如数据竞争和可见性问题。如果多个线程对共享变量进行读写操作,并且存在指令重排,那么就可能导致线程之间读取到的变量值不一致,从而产生错误的结果。
为了解决指令重排带来的问题,可以使用同步机制,如volatile关键字或synchronized关键字。这些机制可以禁止或限制指令重排,保证线程之间的执行顺序和结果的正确性。
在面试中,指令重排是一个常见的话题,面试官可能会询问你对指令重排的理解以及如何避免相关问题。了解指令重排的原因和解决方法,可以帮助你更好地理解多线程编程中的并发问题。
需要注意的是,volatile关键字只能保证可见性,不能保证原子性。如果需要保证原子性,可以考虑使用synchronized关键字或者Atomic类。 💕💕(记住这个重点,一般面试中面试官会问到)💕💕
5、可见性(面试必问)
volatile关键字可以确保多线程环境下共享变量的可见性。可见性是指当一个线程修改了某个共享变量的值时,其他线程能够立即看到这个修改后的值。
在没有使用volatile关键字的情况下,线程在读取共享变量时,可能会从自己的工作内存中读取变量的值,而不是从主内存中读取。这样就有可能导致一个线程对共享变量的修改对其他线程不可见,从而引发并发问题。
而使用volatile关键字修饰共享变量时,当一个线程修改了该变量的值,会立即将修改后的值刷新到主内存中,而其他线程在读取该变量时,会直接从主内存中读取最新的值,从而保证了可见性。
6、读写屏障(面试必问)
读写屏障(Memory Barrier)是一种硬件指令或者编译器插入的指令,用于确保在执行屏障之前的所有读写操作都完成,并将结果刷新到主内存中,然后再执行屏障之后的读写操作。读写屏障可以用来保证多线程环境下共享变量的可见性和一致性。
读写屏障有两种类型:读屏障和写屏障。
读屏障(Read Barrier)用于确保在读屏障之前的所有读操作都已完成,并将结果刷新到主内存中。读屏障可以防止指令重排序,保证后续的读操作不会读取到过期的值。
写屏障(Write Barrier)用于确保在写屏障之前的所有写操作都已完成,并将结果刷新到主内存中。写屏障可以防止指令重排序,保证后续的写操作不会被提前执行。
在多线程编程中,读写屏障的使用可以保证共享变量的可见性和一致性。当一个线程对共享变量进行写操作时,可以使用写屏障来确保该操作对其他线程可见。当一个线程对共享变量进行读操作时,可以使用读屏障来确保读取到的值是最新的。
7、volatile 的代码案例
使用 java 写一个案例:
package com.pany.camp.volatiles;
/**
* @description: Volatile
* @copyright: @Copyright (c) 2022
* @company: Aiocloud
* @author: pany
* @version: 1.0.0
* @createTime: 2023-07-01 18:57
*/
public class VolatileExample {
private volatile boolean flag = false;
public void setFlag(boolean value) {
flag = value;
}
public boolean getFlag() {
return flag;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
// 线程A修改flag的值为true
Thread threadA = new Thread(() -> {
example.setFlag(true);
});
// 线程B读取flag的值
Thread threadB = new Thread(() -> {
boolean flag = example.getFlag();
System.out.println("Flag value: " + flag);
});
threadA.start();
threadB.start();
}
}
我们创建了一个名为VolatileExample的类,其中包含一个volatile修饰的boolean类型的变量flag。在主线程中,我们创建了两个线程,线程A负责修改flag的值为true,线程B负责读取flag的值并打印出来。
由于flag是使用volatile修饰的,线程B在读取flag的值时会直接从主内存中读取,而不会从自己的工作内存中读取。这样可以确保线程B看到的flag值是线程A修改后的最新值,而不是过期的值。
💕💕 本文由激流原创,原创不易,感谢支持
💕💕喜欢的话记得点赞收藏啊