第一篇就决定谈谈关于volatile(改博客期间突然想到之前关于此问题的回答,觉得不是很全面,顺便稍加补充)

原回答

多线程操作三个基本特性—原子性、可见性、有序性
volatile可以保证其中的可见性、有序性,但不能保证原子性(锁)

可见性

提到可见性必须了解java内存模型(JMM),即线程间共享内存和线程独占内存的缓存一致性问题

如下图,未添加volatile关键字的共享变量n,假设线程1先运行,线程1在进行++n操作后将n的值更新为1,线程2在进行++n操作时应该得到的初始值为n=1,但是由于没有添加volatile关键字,导致线程2在获取n的值时,线程1的更新并未刷写到主内存中,于是两个线程运行完的结果n=1并非为预期值。

保证线程对变量或对象的改变可以立刻刷写到主内存中

image-20240202162619052

有序性

指令重排:JVM指令重排和CPU指令重排

一般JVM指令重排不会导致程序运行出现错误,但是cpu不会考虑语言特性,无法识别java的多线程操作,为了cpu提升性能进行重排的指令可能会导致程序崩溃

volatile作为标识,在运行过程中调用本地方法栈的native方法操作cpu,禁止cpu指令重排

补充

如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序

在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:

1
2
3
public native void loadFence();
public native void storeFence();
public native void fullFence();

双重校验锁实现对象单例(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

两次判空

第一次,所有调用该方法的线程同事校验uniqueInstance是否为null,为null的进入判断,但是只有一个线程可以拿到锁,其他线程阻塞等待

第二次,假设第一个未初始化对象初始化完成释放锁,这时uniqueInstance已创建实例,但是阻塞等待的线程已经通过第一次判空检测,所以拿到锁之后需要进行第二次判空,判断是否之前有线程已经初始化uniqueInstance,因为需要在第一次初始化成功后立刻刷新uniqueInstance实例信息到主内存,所以必须用volatile修饰