1. 保证线程可见性
在Java内存模型(JMM)中规定,数据存储在主存中,当线程需要数据时,拷贝一份到自己的工作内存中,进行操作,结束后写回主存。以上规定都是理论模型,不一定对应真实实现,但其中的逻辑是一致的。由于都是拿到的拷贝,所以线程之间的修改可能会产生并发问题。
volatile正好就是解决这个问题的关键。volatile修饰的引用,在内容修改后,会通知其他含有该数据的线程他们手中的数据已过期。其他线程就会重新取新的数据。
2. 不保证原子性
volatile无法保证原子性的含义主要是指无法保证诸如i++这样的操作的原子性。
因为Num++可以分为:读取Num的值,将Num的值+1,写入最新的Num的值
三个步骤的话就无法保证这整个操作的原子性了。
3. 禁止指令重排
一般情况下,为了高效的执行指令,指令重排是会发生的,重排的一个准则就是保证单线程的情况下执行结果不变。然而在多线程的情况下就容易产生一些问题。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
上述代码是双重校验的单例模式。
如果不加volatile修饰,就可能产生问题,产生问题的是这一行
singleton = new Singleton();
我们知道new一个对象的主要逻辑分为三步
- 开辟一块内存
- 初始化对象
- 设置引用指向内存。
在单线程的情况下,不管是123还是132的执行顺序都不会产生错误,意味着可能会发生指令重排变成132。
多线程的情况下,某个线程先生成了对象,执行顺序为132,在执行完第3步的时候,已经有对象指向了内存空间,所以
singleton != null
如果此时执行别的线程发现不为空,进行操作的时候就会发现错误,因为还未初始化。