volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略
class SoWhat {
volatile int i = 0;// volatile修饰的变量
public int getI() {
return i;// 单个volatile变量的读
}
public void setI(int j) {
this.i = j; // 单个volatile 变量的写
}
public void inc() {
i++;//复合多个volatile 变量
}
}
class SynSoWhat {
int i = 0;
public synchronized int getI() {
return i;
}
public synchronized void setI(int j) {
this.i = j;
}
public void inc() {
// 普通方法调用
int tmp = getI();
// 调用已同步方法
tmp = tmp + 1;
//普通写方法
setI(tmp);
// 调用已同步方法
}
}
volatile写的内存语义如下:
当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存。
public class VolaSemanteme {
int a = 0;
volatile boolean flag = false;
// 这是重点哦
public void init() {
a = 1;
flag = true;
//.......
}
public void use() {
if (flag) {
int i = a * a;
}
//.......
}
}
线程A调用init方法,线程B调用use方法。
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。关于内存屏障的具体讲解以前写过不再重复,JMM装逼于无形这里说过。总结来说就是JMM内部会有指令重排,并且会有af-if-serial跟happen-before的理念来保证指令的正确性。内存屏障就是基于4个汇编级别的关键字来禁止指令重排的,其中volatile的规则如下:
转存失败重新上传取消转存失败重新上传取消
JMM对volatile的内存屏障插入策略
在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。
转存失败重新上传取消转存失败重新上传取消
JMM对volatile的内存屏障插入策略
在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。
转存失败重新上传取消转存失败重新上传取消
重点说下volatile读后面为什么跟了个LoadLoad。加入我有如下代码 AB两个线程执行,B线程的flag获取下面的读被提前了。
有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令。在CPU级别的功能如下:
将当前处理器缓存行的数据写回到系统内存
这个写回内存的操作会告知在其他CPU你们拿到的变量是无效的下一次使用时候要重新共享内存拿。
我们可以通过jitwatch对简单的代码进行详细的反汇编看一下。
public class VolaSemanteme {
int unvloatileVal = 0;
volatile boolean flag = false;
public static void main(String[] args) {
VolaSemanteme volaSemanteme = new VolaSemanteme();
volaSemanteme.init();
volaSemanteme.use();
}
public void init() {
unvloatileVal = 1;
flag = true; // 第九行哦
}
public void use() {
if (flag) {
int LocalA = unvloatileVal;
if (LocalA == 0) {
throw new RuntimeException("error");
}
}
}
}
对普通变量的赋值操作:
对volatile变量的赋值操作。
可以对比得出,volatile 修饰的变量确实会多一个 lock addl $0x0,(%rsp) 指令。
0x0000000114ce95cb: lock addl $0x0,(%rsp) ;*putfield flag; - com.sowhat.demo.VolaSemanteme::init@7 (line 9)
使用 volatile 必须具备的条件
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
volatile boolean shutdownRequested;
// ......
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble();// this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff
//...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}
安全使用 volatile 的另一种简单模式是定期 发布 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。
@ThreadSafepublic
class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。 如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
@ThreadSafepublic
class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock
held@GuardedBy("this")
private volatile int value;
public int getValue() {
return value;
}
public synchronized int increment() {
return value++;
}
}
单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
} return instance;
}
}
推荐懒加载优雅写法 Initialization on Demand Holder(IODH)。
public class Singleton {
public static Singleton getInstance() {
return SingletonHolder.instance;
}
static class SingletonHolder {
static Singleton instance = new Singleton();
}
}