关键字:volatile
# 概念
一种解决可见性和有序性问题的方案
# 特点
# 防重排序
懒汉式 - 单例模式 - 双重检查加锁:
public class Singleton {
public static volatile Singleton singleton;
/**
* 构造函数私有,禁止外部实例化
*/
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
在上述单例模式代码中,使用了 volatile
关键字在单例对象上,以防止在极端情况下因操作系统 对指令进行重排序
导致不可预料的结果。
正常实例化对象步骤
- 分配内存空间。
- 初始化对象。
- 将内存空间的地址赋值给对应的引用。
被对指令进行重排序可能发生的步骤
- 分配内存空间。
- 将内存空间的地址赋值给对应的引用。
- 初始化对象。
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为 volatile 类型的变量。
# 实现可见性
package study;
public class VolatileStudy implements Runnable {
private boolean flag = false;
public static void main(String[] args) {
VolatileStudy vs = new VolatileStudy();
new Thread(vs).start();
while (true) {
if (vs.isFlag()) {
System.out.println("---------------");
break;
}
}
}
@Override
public void run() {
Thread.sleep(200);
this.flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() { return flag; }
public void setFlag(boolean flag) { this.flag = flag; }
}
运行结果
flag = true
在上述代码中,由于 main 线程中的 while(true)
速度极快 (opens new window),以至于无法从主存中获取最新的值,因此 main 线程一直不能执行到 System.out.println("---------------");
。
解决方法一:打破 while(true)
的 “速度极快”
在 while 循环末尾添加一个休眠代码,则能读取主存更新后的值。
解决方法二:synchronized 关键字
synchronized (vs) {
if (vs.isFlag()) {
System.out.println("---------------");
break;
}
}
解决方法三:volatile 关键字
private volatile boolean flag = false;
总结
上述场景中,由于 (主) 线程没看到其他线程对共享变量值的修改,导致 BUG 的出现。使用 volatile
关键字实现可见性,从而避免上述 BUG 的产生。
# 不保证原子性
volatile 不能保证完全的原子性,只能保证单次的读 / 写操作具有原子性。
似乎有没有
volatile
关键字都可以保证单次读 / 写的原子性,难道这还可以细分吗?@NipGeihou
如何解决i++原子性问题
仅仅依靠 volatile
关键字无法解决 “i++ 原子性” 问题,原因是 i++ 实际上分为读、写两步操作,可通过使用 原子类
或 Synchronized关键字
解决此问题。
# 原子变量与 CAS 算法
# i++ 原子性问题
package study;
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable {
private int serialNumber = 0;
@Override
public void run() {
Thread.sleep(200);
System.out.println(Thread.currentThread().getName() + ":" + getSerialNumber());
}
public int getSerialNumber() { return serialNumber++; }
}
输出结果
Thread-5:0
Thread-9:2
Thread-0:1
Thread-2:2
Thread-7:5
Thread-3:4
Thread-4:3
Thread-8:1
Thread-1:0
Thread-6:6
进程已结束,退出代码0
上述代码出现原子性线程安全问题
解决方法:使用原子变量
...
// private int serialNumber = 0;
private AtomicInteger serialNumber = new AtomicInteger();
...
public int getSerialNumber() {
// return serialNumber++;
return serialNumber.getAndIncrement();
}
...
# 原子变量
java.util.concurrent.atomic
[标量类]
AtomicBoolean
AtomicInteger
AtomicLong
AtomicReference
[数组类]
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
[更新器类]
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
[复合变量类]
AtomicMarkableReference
AtomicStampedReference
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
上述原子变量使用了:
volatile 保证内存可见性
CAS(CompareAndSwap)算法保证数据的原子性
CAS 包括了三个操作数:内存值 V 、预估值 A、更新值 B;当且仅当 V == A 时, V = B。