4.4 volatile的应用场景

4.4.1 正确使用volatile条件

条件一: 写入变量时并不依赖变量的当前值;或者能够确保只有单一线程能够修改变量的值
条件二: 变量不需要与其他的状态变量共同参与不变约束
条件三: 变量访问不需要额外加锁
通俗点: 当一个变量依赖其他变量或变量的新值依赖旧值时,不能用volatile

4.4.2 volatile使用场景

适用场合:多个线程读,一个线程写的场合
使用场景:通常被 作为标识完成、中断、状态的标记,值变化应具有原子性
充分利用其可见性:即volatile能够保证在读取的那个时刻读到的肯定是最新值
重点声明: volatile主要使用的场合是在多线程中可以感知实例变量被变更了,并且可以获得最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用,但不能保证你在使用最新值过程中最新值不发生变化!很可能在使用之后,最新值已经变更。原数据变成过期数据,这时候就会出现数据不一致(非同步)的问题

4.4.3 正确使用volatile

状态标志

//使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
//使用理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换

//例子:判断业务是否结束
volatile boolean isOk = false;
public void isOk() { isOk = true; }
public void doWork() {
//循环监听状态位变化
while (!isOk) {
// do work
}
}

独立观察

//使用:将 volatile变量用于多个独立观察结果的发布
//特点:是"状态标志"的拓展,该值随时会发生变化,同时会被反复使用,前者一般就是用一次

//使用理由:只是简单的赋值操作,不会做复合操作
//例子:将新节点作为最后一个节点
class CustomLinkedList{
public volatile Node lastNode;
.....
public void add() {
Node node = new Node();
.....
lastNode = node;//将新节点作为最后一个节点
}
}

开销较低的读-写锁策略

//使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
//使用理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性

@ThreadSafe
public class Counter {
private volatile int value;
public int getValue() { return value; }//利用volatile保证读取操作的可见性
public synchronized int increment() { //利用synchronized保证复合操作的原子性
return value++;
}
}

一次性安全发布

//双重检查锁定:实现线程安全的延迟初始化,同时降低同步开销

//    1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
// 2.对象创建完毕,执行get方法将不需要获取锁,直接返回创建对象
//隐患:多线程环境下,由于重排序,该对象可能还完成初始化就被其他线程读取
//
public class DoubleSynchronizedSingleton {
private static DoubleSynchronizedSingleton doubleSynchronizedSingleton;
private DoubleSynchronizedSingleton(){
}
//双重锁设计
public static DoubleSynchronizedSingleton getInstance(){
if (doubleSynchronizedSingleton == null){
//1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
synchronized (DoubleSynchronizedSingleton.class){
if (doubleSynchronizedSingleton == null){
//隐患:多线程环境下,由于重排序,该对象可能还完成初始化就被其他线程读取
doubleSynchronizedSingleton = new DoubleSynchronizedSingleton();//问题代码
}
}
}
//2.对象创建完毕,执行get方法将不需要获取锁,直接返回创建对象
return doubleSynchronizedSingleton;
}
}

单线程环境下(或者说正常情况下),在"问题代码处",会执行如下操作,保证能获取到已完成初始化的实例
image_1bnnou6sdvnmb4g1rprg0a10gt1s.png-16.8kB

隐患:多线程环境下,在"问题代码处",会执行如下操作,由于重排序导致2,3乱序,后果就是其他线程得到的是null而不是完成初始化的对象
image_1bnnougikstm13ctbdpojk4jc29.png-21.8kB

//基于volatile的解决方案
public class SafeDoubleCheckSingleton {
//通过volatile声明,实现线程安全的延迟初始化
private volatile static SafeDoubleCheckSingleton singleton;
private SafeDoubleCheckSingleton(){
}
public static SafeDoubleCheckSingleton getInstance(){
if (singleton == null){
synchronized (SafeDoubleCheckSingleton.class){
if (singleton == null){
//原理利用volatile在于 禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
singleton = new SafeDoubleCheckSingleton();
}
}
}
return singleton;
}
}

参考

​并发番@Java内存模型&Volatile一文通(1.7版)​​​​volatile的应用场景​​​​​

​Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)​