介绍
volatile是java虚拟机提供的轻量级的同步机制。
三大特性:
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM内存模型之可见性
JMM:java内存模型是一种抽象概念,并不真实存在,他描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关闭同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程解锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
可见性代码验证
可见性:当本线程修改了自己本地内存的共享变量值,并写回给主内存,其他线程第一时间就会知道该值修改了。
public class VolatileDemo {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" come in");
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e) {
e.printStackTrace();
}
data.add();
System.out.println(Thread.currentThread().getName()+" updated num value:"+data.number);
},"aaa").start();
while (data.number==0){
//main线程一直等待循环,直到number值不等于0
}
System.out.println(Thread.currentThread().getName()+" mission is over,value:"+data.number);
}
}
class Data{
int number = 0;
public void add(){
this.number = 60;
}
}
当线程修改了number值后,发现主线程并没有感知到number的变化。
修改并执行
volatile int number = 0;
这里就证明了volatile的可见性。
原子性
volatile不保证原子性。
原子性解释:不可分割,完成性。某个线程在做某个具体业务时,中间业务不可以被加塞或者被分割,需要整体完成,要么同时成功,要么同时失败。
案例演示代码:
class Data{
volatile int number = 0;
public void addPlus(){
this.number++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
Data data = new Data();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
data.addPlus();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" number value:"+data.number);
}
}
按照正常逻辑,20个线程一起跑,最后结果应该是20000,经过我长时间测试,极少情况下number最终等于20000,绝大部分都是小于20000的结果。
所以volatile不保证原子性。
不保证原子性的解释
public class T1 {
volatile int n =0;
public void add(){
n++;
}
}
先配置idea external tools
配置成功后,右击,执行javap -c
以下是打印结果:
Compiled from "T1.java"
public class com.wj.volat.T1 {
volatile int n;
public com.wj.volat.T1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field n:I
9: return
public void add();
Code:
0: aload_0 //从局部变量0中装载引用类型值
1: dup //复制栈顶部一个字长内容
2: getfield #2 //获取n值 // Field n:I
5: iconst_1 //将常量压入栈中
6: iadd //加1操作
7: putfield #2 //将增加后的值写回 // Field n:I
10: return
}
n++被拆分成三个指令:getfield,iadd,putfield。
当一个线程修改完变量值后,正准备向主内存写入变量值,却不幸被挂起了,别的线程又去修改变量值并写入主内存,但是原来那个线程并不知道,又被唤醒执行,写入之前的修改值,导致最新修改值丢失。最终导致小于20000。
解决不保证原子性
第一种(不推荐):给add方法添加同步synchronized。
第二种:juc包下的Atomic工具
class Data{
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndAdd(1);
}
}
public class VolatileDemo {
public static void main(String[] args) {
Data data = new Data();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
data.addAtomic();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" number value:"+data.atomicInteger);
}
}
使用AtomicInteger后就可以保证原子性
禁止指令重排
计算机在执行程序时,为提高性能,编译器和处理器的常常会对指令做重排.
分为以下三种:
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
- 处理器在进行指令重排序时必须要考虑指令之间的数据依赖性
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果不可预测
出于性能考虑,JVM和CPU是允许对程序中的指令进行重排的,只要保证(重排后的)指令语义一致即可。如下代码为例:
int a = 1;
int b =2 ;
a++;
b++;
这些指令可以按以下顺序重排,而不改变程序的语义:
int a = 1;
a++;
int b =2;b++;
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:保证特定操作的执行顺序和保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就足说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。