核心作用:解决可见性和有序性问题
volatile
的主要作用可以归结为两点:
1.保证变量的可见性 和 禁止指令重排序。
2.它提供了一种轻量级的同步机制,
3.但需要注意的是,它不能保证原子性。
保证可见性:
什么是可见性问题?
1.在 Java 内存模型中,每个线程都有自己的工作内存。
2.线程操作变量时,通常会先从主内存拷贝一份到自己的工作内存,
操作完成后再刷回主内存。
问题在于:一个线程修改了自己工作内存中的变量副本,在没有同步机制的情况下,其他线程可能无法立即看到这个修改,读到的仍然是旧值。
volatile
如何解决?
写操作:当写一个 volatile
变量时,
JVM 会立即将该线程工作内存中的新值强制刷新到主内存中。
读操作:当读一个 volatile
变量时,
JVM 会使该线程工作内存中的缓存失效,
从而强制它从主内存中重新读取最新的值。
这样就保证了,一旦某个线程修改了 volatile
变量,这个新值对其他所有线程来说都是立即可见的。它解决了线程间的可见性问题。
禁止指令重排序
什么是指令重排序问题?
1.为了优化性能,编译器和处理器常常会对指令的执行顺序进行重排序(在不改变单线
程程序语义的前提下)。
2.在单线程下没问题,但在多线程环境下,这种乱序执行可能导致意想不到的错误。
volatile
如何解决?
过添加内存屏障(Memory Barrier)来实现。
在写 volatile
变量时,会在写操作后插入一个“写屏障”,
确保该变量之前的所有操作都已经完成,并且结果对其他线程可见。
这些屏障阻止了编译器和水线将 volatile
变量的操作与其他内存操作进行重排序,
从而保证了有序性。
不能保证原子性
这是一个非常关键的限制,也是 volatile
最常见的误用点
什么是原子性?
原子性意味着一个操作是不可中断的,
要么完全执行成功,要么完全不执行。
例如 i++
这个操作,它实际上包含三个步骤:
读取 i
的值
将 i
的值加 1
将新值写回 i
这是一个复合操作,而不是原子操作。
为什么
volatile
不行?
假设两个线程同时执行 volatileInt++
(volatileInt
是 volatile
变量):
线程 A 读取
volatileInt
的值为 10。线程 B 也读取
volatileInt
的值为 10(因为线程 A 还没写回,可见性保证了读到的都是最新值,但读取后它们就各自操作了)。线程 A 对 10 加 1 得到 11,并立即写回主内存(由于是
volatile
,写成功)。线程 B 也对 10 加 1 得到 11,并写回主内存(覆盖了线程 A 的结果)。
最终结果不是预期的 12,而是 11。
可见性保证了它们读到的都是最新的,但无法阻止它们在“读取-修改-写回”这个过程中被中断。要解决这个问题,必须使用锁(synchronized
)或原子类(如 AtomicInteger
)。