开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第30天,点击查看活动详情
引言
最近看动态代理源码的时候,发现 Method.invoke() 的执行委托类 MethodAccessor 被 volatile 修饰,不是很理解,就先整理一下 Java volatile 的特点。
volatile 修饰变量特点
先看 Java 被 volatile 修饰的变量的特点:
-
被 volatile 修饰的变量,在 CPU 主存中被访问,即此变量存储在 CPU 的主存中,而不是 CPU 的缓存
-
Java volatile 关键词保证变量在线程中的可见性,适用的场景为:
public class ShareVolatileObject { public volatile int counter = 0; }当有两个线程(Thread 1 as T1,Thread 2 as T2),T1 修改 counter,T2 读取 counter,并且随后 T2 保证只读。这个场景下,volatile 可以保证并发的安全性。但是当 T1 T2 同时增加修改 counter时,是不能保证并发安全性的。
-
当一个对象的 volatile 变量被写时,当前线程中的此对象的所有变量,都会被写进主存中。
相似的,当一个对象的 volatile 变量被读取时,当前线程中的此对象的所有变量,也同样会从主存读取。
public class ShareVolatileObject {
private int years;
private int months;
public volatile int days;
public void update(int years, int months, int days) {
this.years = years;
this.months = months;
this.days = days;
}
public int totalDays() {
int total = this.days;
return total + years * 365 + months * 30;
}
}
也就是说,当调用 update() 或者 totalDays(),所有变量的值都会在主存中刷新。
-
因为 Java volatile 修饰变量被读或者被写时,使对象中其他变量的同时从主存读入或者写入主存这个特性,与 JVM 和 CPU 在不影响语义的正确性的同时进行指令的重排序这个特点在某些情况下会影响到并发的正确性。
就拿 update() 这个函数来说,其中 this.days = days 的执行顺序,会影响到主存中 years 以及 months 的值。
-
Happens-Before
Java volatile keyword 给出 Happens-Before 原则来解决指令重排序的问题,来保证变量的可见性的问题。
- 如果最初对其他变量的读写操作发生在对 volatile 修饰的变量写之前,那么不允许将对其他变量的写指令重排序到对 volatile修饰的变量写之后。但是如果这个写操作发生在,对 volatile 变量写之后,那么这个写操作是允许重排序到对 volatile 排序之前。
- 如果最初的读操作发生在对 volatile 修饰的变量进行读操作之后,那么读写的指令,不能重排序在对于 volatile 变量的对操作指令之前。注意对于其他变量的读写发生在读 volatile 变量之前,那么仍有可能被重排序到读 volatile 变量之后。
-
Volatile 变量的局限性
当多线程同时多一个共享变量进行读写操作时(经典栗子,多线程进行求和,虽然每个线程都是从主存中读取数据,但是存在写入的时间间隔),volatile 无法保证其正确性,应该使用 synchronized 或者使用 atomic data types, e.g AtomicLong or AtomicReference
-
Volatile 适用的情况
如果,只有一个线程进行对 volatile 变量进行读写,其他线程对 volatile 变量只读,那么读的线程就可以保证,始终从主存中读到最新的 volatile 变量。如果不用 volatile 修饰,不能保证变量的可见性。
-
Volatile 变量的性能考虑
- volatile 变量是从主存中读取,而不是 CPU cache 代价是昂贵的。
- Volatile 变量的 happens-before 原则也会影响 JVM and CPU 的指令重排序,也会影响性能。
所以要慎重使用volatile变量,只有当你真正需要的时候!