Volatile
- 1. 概念介绍
- 2. 可见性问题背景
- 3. `@Volatile`的工作原理
- 4. 与其他同步机制的比较
- 5. 防止指令重排序(在一定程度上)
- 参考地址
1. 概念介绍
@Volatile
是Java中的一个关键字,用于修饰变量。它主要用于解决多线程环境下变量的可见性问题。当一个变量被声明为volatile
时,这意味着该变量的值在多个线程之间是“可见”的。
2. 可见性问题背景
- 在多线程环境中,如果没有适当的同步机制,一个线程对共享变量的修改可能不会及时被其他线程看到。这是因为每个线程可能会在自己的工作内存(缓存)中保留变量的副本。例如,线程A修改了一个共享变量的值,但线程B可能仍然在使用它自己缓存中的旧值,直到某个不确定的时间点才会更新这个值。
3. @Volatile
的工作原理
- 当一个变量被标记为
volatile
时,每次读取这个变量时,线程会直接从主内存中读取,而不是从自己的工作内存中读取。同样,每次修改这个变量时,线程会立即将新值刷新到主内存中。这样就保证了其他线程能够及时看到变量的最新值。 - 例如,假设有一个共享的
volatile
变量count
:public class VolatileExample {@Volatileprivate static int count = 0;public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {count++;}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {count++;}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: " + count);} }
- 在这个例子中,
count
变量被声明为volatile
。两个线程thread1
和thread2
都对count
进行自增操作。由于count
是volatile
的,每个线程在修改count
后会立即将新值刷新到主内存,并且在读取count
时会从主内存读取最新的值。不过需要注意的是,volatile
并不能保证原子性,在这个例子中,count++
操作实际上包含了读取、修改和写入三个步骤,可能会出现并发问题(最终的count
值可能小于2000),但它确实保证了可见性。
4. 与其他同步机制的比较
- 与
synchronized
的比较:synchronized
关键字不仅保证了变量的可见性,还保证了操作的原子性。它通过对代码块或者方法进行加锁,使得同一时间只有一个线程能够访问被锁定的部分。而volatile
主要关注的是变量的可见性,对于复杂的复合操作(如count++
),它不能保证原子性。- 例如,使用
synchronized
来保证count++
操作的原子性可以这样做:public class SynchronizedExample {private static int count = 0;public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (SynchronizedExample.class) {for (int i = 0; i < 1000; i++) {count++;}}});Thread thread2 = new Thread(() -> {synchronized (SynchronizedExample.class) {for (int i = 0; i < 1000; i++) {count++;}}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: " + count);} }
- 与
java.util.concurrent.atomic
包的比较:java.util.concurrent.atomic
包提供了一系列原子类,如AtomicInteger
、AtomicLong
等。这些原子类在保证变量可见性的同时,还能保证操作的原子性。例如,AtomicInteger
的incrementAndGet
操作是原子的。
public class AtomicExample {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {count.incrementAndGet();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {count.incrementAndGet();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Count: " + count.get());} }
- 与
volatile
相比,原子类提供了更高级的并发控制功能,适用于需要原子操作的场景。而volatile
更像是一种轻量级的可见性保证机制,用于简单的共享变量场景。
5. 防止指令重排序(在一定程度上)
- 指令重排序介绍:在现代处理器和编译器优化中,为了提高程序执行效率,可能会对指令进行重新排序。在单线程环境下,这种重排序不会影响程序的正确性。但在多线程环境下,可能会导致问题。
volatile
关键字可以在一定程度上防止指令重排序。 - 示例场景:考虑一个简单的单例模式实现(双重检查锁定模式),在这个模式中需要防止指令重排序。
public class Singleton {private volatile static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {// 创建单例实例instance = new Singleton();}}}return instance;} }
- 在这个例子中,
instance
被声明为volatile
。当创建instance
时(instance = new Singleton();
),这个操作实际上包含了三个步骤:分配内存空间、初始化对象、将对象引用赋值给instance
变量。如果没有volatile
,处理器或编译器可能会对这三个步骤进行重排序。例如,先将对象引用赋值给instance
变量,然后再初始化对象。在其他线程访问instance
时,可能会获取到一个还没有完全初始化的对象,导致程序出现错误。使用volatile
可以防止这种指令重排序,保证了对象在被引用之前已经完成初始化。
参考地址
AI 豆包