当我们聊到volatile的时候,只需要围绕它的三个特性来了解即可
1.保证可见性
2.不保证原子性
3.禁止指令重排
那么我们则需要细化这三个特性的内容
1.volatile是如何保证可见性的呢?
在java中还有一个约定的概念JMM(java内存模型)我们在建立线程的同时,线程将会分为主内存+工作内存
而我们在进行一些共享内存变量(比如静态变量)的一些操作的时候
JMM则有一些同步约定:1.线程解锁前,共享变量必须刷新到主内存中 2.线程加锁前,必须将主内存的共享变量读取到工作内存中 3.加锁和解锁必须是同意把锁
那么这个设定,对于多个线程同时操作共享变量时,则会遇到线程A未运行完毕,线程B已经修改了共享变量的值,而引发一系列问题:这就是共享变量不可见造成的。
所以我们就需要用到volatile来保证共享变量的可见性。通过volatile的修饰,线程A,B能够在内存中看到被修饰的共享变量,而在修改的时候,及时通知运行时的线程,从而避免问题。
public class volatileTest {
//不加 volatile 程序就会死循环
//加volatile 保证了可见性
private volatile static int num = 0;
public static void main(String[] args) {//main线程
new Thread(()->{//线程1
while(num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
2.为什么volatile不保证原子性
在java中我们可以随意用一个例子来进行分析,比如volatile static int num=0; num++;
我们通过javap对于编译的字节码文件进行查看的时候可以发现,编译器实际上把a++分成了多步,首先是静态变量的出栈,然后计算,最后入栈。像这种多步操作往往就会存在线程安全的隐患。从而是非原子性操作。
那我们应该怎样保证变量的原子性呢?
1.加共享锁:lock/syncronized
2.使用原子类
原子类为什么能够保证变量原子性 这里我们需要在CAS中进行详解。
不过我们可以先看下这些原子类的源码
以AtomicInteger为例,实际上在进行计算的时候,AtomicInteger是使用的Unsafe类进行的计算
而在Unsafe类中,我们将会看到大量的native修饰的方法,这些方法则是以c编写的方法,也就是说这些对于变量的计算 都是通过直接在内存中修改变量的值,从而保证原子性的操作。
3.禁止指令重排
什么是指令重排?
在我们的操作系统中,对于源码可能会有以下操作 1.编译器优化重排 2.指令并行重排 3.内存系统重排
这样的话 将会导致一个问题,我们所编写的程序指令,可能不会按照我们想要的顺序来执行
那么这个时候,我们就要用到volatile来修饰变量,来禁止指令重排
可以看到,这个时候在内存中的指令可以分为普通指令,和volatile指令,当我们对于变量添加了volatile描述之后
系统会对于volatile指令的前后方添加内存屏障,从而避免指令重排。