一、 对volatile的理解
1. volatile是java虚拟机提供的轻量级的同步机制。
- 保证可见性
- 不保证原子性
- 禁止指令重排
保证可见性
什么是可见性?
JMM(java内存模型)
JMM是一个抽象的概念本身不存在,它描述的是一组规范,通过这组规范定义了程序中各个变量的访问方式。
- 可见性
- 原子性
- 有序性
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
JMM可见性图解:
volatile保证可见性的代码证明:
没有添加volatile关键字修饰
package com.jess.juc;
import java.util.concurrent.TimeUnit;
class Data {
int number = 0;
public void add(){
this.number = 60;
}
}
public class VolatileDemo{
public static void main(String[] args) {
// 假如int number = 0; number变量之前没有添加volatile关键字修饰
Data data = new Data();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 线程启动");
//暂停一下线程 3s
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.add();
System.out.println(Thread.currentThread().getName()+" 线程更新:"+data.number);
},"A").start();
//main线程
while (data.number == 0){
//等待,知道number不在等于0
}
//成功打印说明main线程感知到number值变了,体现了可见性
System.out.println(Thread.currentThread().getName()+" 线程" + "任务完成");
}
}
main线程并没有打印任务完成,说明没有感知到number值发生了变化
添加volatile关键字修饰
class Data {
volatile int number = 0;
public void add(){
this.number = 60;
}
}
volatile 可见性的实现
- 在生成汇编代码指令时会在 volatile 修饰的共享变量进行写操作的时候会多出 Lock 前缀的指令
- Lock 前缀的指令会引起 CPU 缓存写回内存
- 一个 CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效
- volatile 变量通过缓存一致性协议保证每个线程获得最新值
- 缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改
- 当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存
不保证原子性
什么是原子性?
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
添加volatile关键字修饰
package com.jess.juc;
class Data {
volatile int number = 0;
public void sub(){
number -= 1;
}
}
public class VolatileDemo{
public static void main(String[] args) {
//验证volatile不保证原子性
Data data = new Data();
//生成20个线程
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
data.sub();
}
},String.valueOf(i)).start();
}
//需要等待20个线程都完成sub后,在执行main线程
//后台默认2个线程,一个是main,一个是gc线程
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" number:"+data.number);
}
}
若volatile可以保证原子性,那么number最终值应该是 -2000
volatile为什么不保证原子性?
num初始为0,可能会出现线程1,2,3线程在更新了数据(+1)后都要写回主内存的时侯。因为CPU调度使得1,2线程挂起,执行3线程的写入操作(num=1)。此时3线程更新的数据写入了主内存,然后1,2线程再进行写入,覆盖了num的值,num的值依然为1(还没来得及收到num的更新通知)。若保证原子性num应该为3。(即出现了丢失写值的情况)
(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。
如何解决原子性?
- 加synchronized(不推荐)
- 如下代码所示
package com.jess.juc;
import java.util.concurrent.atomic.AtomicInteger;
class Data {
volatile int number = 0;
public void sub(){
number -= 1;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void subMyAtomic(){
atomicInteger.getAndDecrement();
}
}
public class VolatileDemo{
public static void main(String[] args) {
//验证volatile不保证原子性
Data data = new Data();
//生成20个线程
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
data.subMyAtomic();
}
},String.valueOf(i)).start();
}
//需要等待20个线程都完成sub后,在执行main线程
//后台默认2个线程,一个是main,一个是gc线程
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" number:"+data.atomicInteger);
}
}
禁止指令重排
计算机在执行程序时,为了能提高性能,编译器和处理器常会对指令进行重排。
简单来说,指令重排就是程序指令的执行顺序有可能和代码的顺序不一致。
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
- 编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
volatile怎样禁止指令重排呢?
内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)