volatile
在 Java
语言中是一个关键字,用于修饰变量。被 volatile
修饰的变量后,表示这个变量是在多个线程中是共享的,编译器在编译时会注意到这个变量是共享的,因此不会对这个变量进行重排序。
可见性:保证了多线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对于其他线程来说是立即可见的。
有序性:禁止进行指令重排序
volatile
并不能保证原子有序性
volatile
变量是基于内存屏障(Memory Barrier
)实现的。
内存屏障,又是内存栅栏,是一个 CPU
指令.
在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序, JMM
为了保证在不同编译器和 CPU
上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障来告诉编译器和 CPU
,不管什么指令都不能和这条内存屏障(Memory Barrir
)指令,进行指令重排。
其他参照
https://www.jianshu.com/p/ccfe24b63d87
LoadLoad
Load1;LoadLoad;Load2;
对于上面的语句,在Load2以及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore
Store1;StoreStore;Store2;
对于上面的语句,在Store2以及后续写入操作执行前,要保证stroe1的写入操作对其他处理器可见。
LoadStore
Load1;LoadStore;Store2;
在Store2及后续写入操作刷出前,保证load1要读取的数据读取完毕。
StoreLoad
Store1;LoadStore;Load2;
在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
sfence:在sfence指令前的写操作必须在sfence指令后的写操作前完成
lfence:在lfence指令前的读操作必须在lfence指令后的读操作前完成
mfence:在mfence指令前的读写操作必须在mfence指令后的读写操作前完成
原子指令,如x86上的lock ...
指令是一个full barrier,执行时会锁住内存子系统来确保,执行顺序,甚至跨多个CPU。软件级别的锁通常使用内存屏障或者原子指令,来实现变量的可见性和顺序。这点也与缓存行刷新有关。
以下为使用 volatile
必须具备的条件:
对变量的写操作比依赖与当前值
该变量没有包含在具有其他变量的不变式中。
说明:多个 volatile
变量不能用于约束条件
示例:
public class NumberRange {
private volatile int lower;
private volatile int upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
上面是一个非线程安全的数值范围类。它包含了一个不变式 - 下界总是小于或等于上界。上述代码中如两个线程在同一时间不一致调用 setLower
与 setUpper
方法则会使范围不一致的状态。所以这里需要将这两个方法原子化。
具体使用场景如下:
状态标志
一次性安全发布(one-time safe publication)
独立观察(independent observation)
volatile bean 模式
开销较低的读-写锁策略
双重检查(double-checked)
也许实现 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
属性。
@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
volatile
的功能还不足以实现计数器。因为 ++x
实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile
计数器执行增量操作,那么它的更新值有可能会丢失。
如果读操作远远超过写操作,可以结合使用内部锁和 volatile
变量来减少公共代码路径的开销。
安全的计数器使用 synchronized
确保增量操作是原子的,并使用 volatile
保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile
读操作,这通常要优于一个无竞争的锁获取的开销。
@ThreadSafe
public 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;
}
}
编译器执行 new Singleton()
方法时将其分为三个指令,
1.分配内存
2.执行Singleton构造函数
3.将实例指向分配的内存
如果不使用 volatile
关键字,CPU
可能会将指令重排, 如 1 > 3 > 2,此时有两个线程 A
B
。
如果 A
线程执行完 1 3 后,这时 instance
指向内存地址,此时 B
线程获取 instance
的值不为 null
但指向一个未实例化完成的对象,所以会出现异常。
推荐懒加载优雅写法 Initialization on Demand Holder(IODH)。
public class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。