特点
- 不会造成线程的堵塞,也不会保证线程的安全性。
2. 可以保证多个线程所看到的值是一致的。
3. 禁止指定重排。
4. volatile只能保证可见性,不能保证原子性。
如何保证变量的可见性?
答案1:
在 Java 中,volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
答案2: 在对volatile修饰的变量进行写操作时,通过编译器生成反汇编指令后,会发现会多一条Lock前缀,就是由于这条Lock前缀所实现的可见性。Lock前缀在多核处理器中会引发下面这两件事情:
Lock指令会将当前处理器缓存行的数据写回到主内存。(ps:每个处理器都有自己的cache缓存,每次缓存中操作的变量都是主内存中变量的拷贝)
一个处理器写回主内存的操作会造成其他处理的缓存无效。
底层实现了内存屏障。
如何禁止指令重排序?
在 Java 中, volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
public native void loadFence();
public native void storeFence();
public native void fullFence();
理论上来说,你通过这个三个方法也可以实现和volatile禁止重排序一样的效果,只是会麻烦一些。
答案2: 单线程不会存在这个问题, 主要是出现在多线程并发的情况,编译器不考虑多线程之间的语义,多线程在执行的时候可能会出现变量读写顺序问题。
通过volatile读写都加了内存屏障来实现禁止指令重排
· LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2, 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
· StoreStore屏障:对于这样的语句Store1; StoreStore; Store2, 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
· LoadStore屏障:对于这样的语句Load1; LoadStore; Store2, 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
· StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2, 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
volatile 可以保证原子性么?
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
很多人会误认为自增操作 inc++ 是原子性的,实际上,inc++ 其实是一个复合操作,包括三步:
- 读取 inc 的值。
- 对 inc 加 1。
- 将 inc 的值写回内存。
volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:
- 线程 1 对 inc 进行读取操作之后,还未对其进行修改。线程 2 又读取了 inc的值并对其进行修改(+1),再将inc 的值写回内存。
- 线程 2 操作完毕后,线程 1 对 inc的值进行修改(+1),再将inc 的值写回内存。
这也就导致两个线程分别对 inc 进行了一次自增操作后,inc 实际上只增加了 1。
其实,如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、Lock或者AtomicInteger都可以。
为什么没有原子性呢?
原子性:
(1) 原子的意思代表着——“不可分”;
(2) Java中只有对基本类型变量的赋值和读取是原子操作
(3) 在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
为什么没有原子性呢?
其实要分成3步:1)读取volatile变量值到local; 2)增加变量的值;3)把local的值写回
有可能在写回到原子的过程中,别的线程拿到的还是旧的值,如果用Atomic cas算法添加了一个比较校验就可以避免这种情况。
volatile和synchronized
1. volatile是变量修饰符,而synchronized则作用于一段代码或方法。
2. volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值, 显然synchronized要比volatile消耗更多资源。
3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
4. volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
双重校验锁实现对象单例(线程安全)
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;
}
}