volatile关键字是java虚拟机提供的最轻量级的同步机制,用来修饰变量,可以保证变量线程间的可见性且禁止指令重排序。
为了更好的理解volatile关键字,先来说下java内存模型
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。
注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
Java内存模型规定所有的变量都是存在主存当中
(类似于前面说的物理内存),每个线程都有自己的工作内存
(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存
public class myThread7 implements Runnable {
//可以对比一下有无volatile的情况
/*volatile*/ boolean running = true;
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行");
while (running){
}
System.out.println(Thread.currentThread().getName()+"结束执行");
}
public static void main(String[] args) {
myThread7 m = new myThread7();
new Thread(m,"t1").start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
m.running = false;
}
}
ps:不用volatile的情况下,程序可能也会正常结束,但不是一定可以正常结束,使用volatile的作用就是保证在多线程情况下程序也能正常结束.
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
双检查单例模式是volatile使用场景里一个比较简单的例子
/**
* Created by IntelliJ IDEA
*
* @author manzuo
* @date 2020/5/17 2:33
*/
public class Singleton {
//必须用volatile修饰
//防止实例化Singleton的时候,指令被重排序
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance==null){
synchronized (Singleton.class){
if (instance==null){
instance = new Singleton();
/*new SingLeton()的操作分成三步
*1. 申请一块内存
*2. 按照构造方法的参数初始化内存(这里省略了这个类的其他参数,在实际业务中会包含其他逻辑以及参数
*3. 将这块内存的地址赋值给instance
* 如果instance不用volatile修饰,上面的相关指令可能会被重排序
* 比如按照 1,3,2的步骤来实例化该类
* 这样new Singleton()的操作还没结束,instance已经不为null,其他线程就可能拿到未初始化的instance
*/
}
}
}
return instance;
}
}
public class myThread8 extends Thread {
volatile int count = 0;
@Override
public void run(){
//每个线程循环10000次count++操作
for (int i = 0; i < 10000; i++) {
count++;
}
}
public static void main(String[] args)
{
myThread8 m = new myThread8();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
//新建10个线程,加入threads中
Thread thread = new Thread(m,"thread-"+i);
threads.add(thread);
}
//逐一启动线程
threads.forEach((thread -> thread.start()));
//在主线程中利用join方法,等待所有方法完成
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//所有子线程运行结束,打印count值
System.out.println(m.count);
}
}
运行发现,count的值总是小于100000. 虽然用volatile变量可以保证count++之后的结果对其他线程可见,但是count++这个虽然看起来只有一句语句,但本身并不是原子操作.
count++执行的时候有三个原子操作:从主存中读取count值到线程自己的工作内存里,在工作内存里进行count+1操作,把最新的count值写回主存里,只有写回到主存里之后,最新的count值才会被其他线程获取.
假设某一时刻,线程1完成count++的操作并写回主存里,此时count=100,然后线程2,线程3同时读取count的最新值是100,同时进行+1操作,同时写回主存里,count值被置为101,而正确的值应该为102.多个线程执行的时候,这种情况会经常发生,因为volatile不能保证对count变量的操作是原子性的.
关于java有序性,补充知识如下:
(摘抄自《深入理解java虚拟机》)
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。