volatile 关键字的原理和要避免的误区
}
如果不用 volatile,则因为内存模型允许所谓的“无序写入”,可能导致失败 。某个线程可能会获得一个未完全初始化的实例。
场景 2
private volatile int value;
//读操作,没有 synchronized,提高性能
public int getValue() {
return value;
}
//写操作,必须 synchronized。因为 x++不是原子操作
public synchronized int increment() {
return value++;
}
这段代码,可实现一个线程间安全的计数器。因为加了 valatile 关键字。每次线程都能取到最新值做加减。
要避免的误区
在代码评审的时候看到 volatile 被滥用的情况。说说我个人的看法:很少变化,对时间不是特别敏感的情况下不建议用 volatile 关键字。
举个例子:从公司的配置中心取到一个配置数据。不建议用 volatile。
一般来说配置中心的架构是下面这个样子
一条数据从用户变更到集中存储的配置中心,配置中心下发到真正使用的机器上,之前公司是要经过 90s(客户端 90s 为周期定时去配置中心取最新数据)。
加了 volatile 关键字在这种场景只是能更快的看到这个最新值而已。下面我们来测试下这个【更快】有多久。
public class VolatileTest {
private boolean endRun = false;
@Test
public void noVolatile() throws Exception {
Runnable r1 = new Runnable() {
public void run() {
int i = 0;
while (!endRun) {
System.out.println("I am still running" + i++);
}
}
};
Runnable r2 = new Runnable() {
public void run() {
endRun = true;
}
};
new Thread(r1).start();
new Thread(r2).start();
Thread.sleep(9000);
System.out.println("end run");
}
}
这个代码里,在第一个线程使用 endRun 这个变量。第二个线程去改变 endRun 这个变量的值。一旦第一个线程看到了第二个线程的值的变化,就会马上停止循环。
运行结果如下:
![volatile 关键字的原理和要避免的误区](https://img-blog.csdnimg.cn/img_convert/fdfc4838c77f956fe4a9dcaedb5d87e
b.png)
说明经过了两个循环的时间,线程就读到了另外一个线程变化的值。对照下面的时间延迟表,我们来计算下:
平均执行一行简单代码要执行 5 个指令。如上执行一个指令需要 1ns。每次循环执行 2 行代码,从运行结果来看共执行了 2 次。共 5_1_2*2=20ns。实际数据应该不是如此,而且是变化的。但是应该都是 ns 级别的。
相比较 90s 的可见性延迟,ns 级别可以忽略不计。
再看看为了早 ns 级看到结果,所花费的开销:volatile 关键字本质是让 L1 缓存、L2 缓存这种 cpu 缓存失效了,直接主存访问。如果要访问的字段在 L1 缓存里,从配置中心取的数据 1 天变化一次。以字段放在 L2 缓存为例。加了 volatile 关键字,访问时间要从 4ns 上升到 100ns,如果这个变量每个请求都要访问,每秒 QPS 是 1000。则 1 天为了取这个数据将多花 1_24_3600_1000_(100-4)ns 约等于 8300ms。
相比获得的收益来讲,代价要大出好几个数量级。但是本身的时间开销本来就很小,坦白说一般的系统一天多花个 8.3s 也是可以接受的。但是这样的变量多了,也是个不小的负担。而且这个负担会随着系统压力增加而加重。
评论