关于作者:CSDN内容合伙人、技术专家, 从零开始做过日活千万级APP。
专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。
目录
- 一、导读
- 二、概览
- 2.1 作用
- 2.2 多线程共享变量的访问流程
- 2.3 多线程为什么会出现可见性问题
- 2.4 volatile如何实现可见性
- 2.5 如何实现禁止指令重排序
- 2.6 举例
- 2.7 来答题
- 三、原理
- 3.2 使用场景:
- 四、 推荐阅读
一、导读
我们继续总结学习Java基础知识,温故知新。
二、概览
volatile 是一个Java关键字,可以用来修饰变量,volatile也被称为轻量级的synchronized,运行时开销比 synchronized更小。
2.1 作用
1、确保共享变量在线程之间的同步,实现可见性。
2、禁止处理器重排序。
2.2 多线程共享变量的访问流程
线程执行时,先拷贝主存数据到本线程本地,操作完成后再把结果从线程本地刷到主存。
我们看下面的图,多线程时,共享变量操作完的值是在红色的区域。
2.3 多线程为什么会出现可见性问题
可见性是由于CPU缓存引起,CPU 增加了缓存,以均衡与内存的速度差异,导致 可见性问题。
在多线程环境下,多个线程同时访问共享变量,由于不同线程的执行顺序和时间不确定,可能会导致一个线程对共享变量的修改在其他线程中不可见。可参考上面共享变量的访问流程。
2.4 volatile如何实现可见性
volatile不允许线程内进行缓存和重排序,直接修改内存,所以对其他线程是可见的。
被volatile修饰的变量读写时,都会直接刷到主存,从而使得变量可见。
2.5 如何实现禁止指令重排序
volatile是通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序的。
当一个变量被修饰时,表示变量是“易变的”(volatile)或者“不稳定的”(unstable),它意味着该变量的值可能会被其他线程(或进程)修改。
注意: volatile 关键字只能保证线程之间的同步,不能保证线程安全。
要保证线程安全,需要使用其他同步机制,比如 synchronized 关键字或者 Lock 接口。
volatile具有可见性、有序性,但不具有原子性,所以是线程不安全的。
2.6 举例
- 不使用volatile关键字
禁止线程缓存变量结果。
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。
引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。
举例:
// Thread-A
new Thread("Thread A") {
@Override
public void run() {
while (!stop) {
}
System.out.println(Thread.currentThread() + " stopped");
}
}.start();
一个线程内使用了停止的开关,假如这个stop没有被volatile修饰,我们在线程b中修改,
线程a并不知道开关的值被修改了。
- 使用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 保证原子性:单次读/写
对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,
但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作,要保证多步的原子性,
可以通过AtomicInteger或者Synchronized来实现,本质上就是cas操作。
2.7 来答题
- 问题1: i++为什么不能保证原子性?
i++其实是一个复合操作,包括三步骤:
* 读取i的值。
* 对i加1。
* 将i的值写回内存。
volatile是无法保证这三个操作是具有原子性的,
我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。
- 问题2: 共享的long和double变量的为什么要用volatile?
因为long和double两种数据类型的操作可分为高32位和低32位两部分,
因此普通的long或double类型读/写可能不是原子的。
因此,鼓励大家将共享的long和double变量设置为volatile类型,
这样能保证任何情况下对long和double的单次读/写操作都具有原子性
三、原理
在JVM底层volatile是采用“内存屏障”来实现的,加入volatile关键字时,会多出一个lock前缀指令
内存屏障,又称内存栅栏,是一个 CPU 指令。
1、用javac命令进行编译生成.class文件,
2、再用javap命令反编译查看.class文件的信息,就可以看到字节码信息中多了一些指令。
为了保证各个处理器的缓存是一致的,实现了缓存一致性协议(MESI),每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
volatile 变量通过这样的机制就使得每个线程都能获得该变量的最新值。
3.2 使用场景:
解决对变量可见性有要求,但是对读取顺序没要求的需求。
- volatile特性:
(1)volatile仅能使用在变量级别
(2)volatile仅能实现变量的修改可见性,不能保证原子性,volatile + cas 就实现了原子性,如atomic包下面的类。
(3)volatile不会造成线程的阻塞
(4)volatile标记的变量不会被编译器优化
四、 推荐阅读
【Java基础】原子性、可见性、有序性
【Java基础】java可见性之 Happens-before
【Java基础】java-android面试Synchronized
【Java基础】java-android面试-线程状态
【Java基础】线程相关
【Java基础】java 异常
【Java基础】java 反射
【Java基础】java 泛型
【Java基础】java注解
【Java基础】java动态代理
【Java基础】Java SPI
【Java基础】Java SPI 二 之 Java APT
【Java基础】 jvm 堆、栈、方法区 & java 内存模型