Volatile
概念:JVM提供的一个轻量级的同步机制
作用:
- 防止JVM对 Long/Double 等64位的非原子性协议进行的 误操作(读取半个数据);
- 可以使某一个变量对所有的线程立即可见(某一个线程如果修改了工作内存中的变量副本,那么加上Volatile关键字之后,该变量就会立即同步到其他线程的工作内存当中)。
- 禁止指令 "重排序" 优化。
前面两点在之前的稳文章中都有提到,下面我们来看什么是指令"重排序"。看指令重排序之前,首先要理解什么是原子性!
原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
-
原子性
num = 10; 就是一个原子操作,这段代码在程序的底层就这么一句话,不会再拆开了!
-
非原子性
int num = 10; 如果现在先定义变量,再赋值。这个操作就是非原子性的。
这段代码在程序底层会拆分成两步,这两步已经不能再拆分了,所以是原子性的。
1、int num;
2、num = 10;
重排序
为了性能优化,编译器和处理器会进行指令重排序。排序的对象就是 原子性 操作!
比如上面的例子,int num = 10不是原子性操作。所以程序会在底层将它变成 int num 和 num = 10,把它变成原子性后在进行重排序。
下面通过一个例子理解重排序,有一个直观的映像:
int a = 10; //1 int a; a= 10;
int b; //2
b = 20; //3
int c = a * b; //4
重排序不会影响单线程的执行规则,因此以上程序在经过重排序后,可能的执行过程为1234或者2314,1234就是按照上面正常的执行流程,2314为
int b; //2
b = 20; //3
int a = 10; //1
int c = a * b; //4
在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码如下:
/**
* @author leizige
*/
public class Singleton {
private Singleton(){
}
public static Singleton instance = null;
public static Singleton getInstance() {
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
/* 不是一个原子性操作 */
instance = new Singleton();
}
}
}
return instance;
}
}
以上代码在并发环境中会出现问题,原因是 instance = new Singleton()
不是一个原子性操作,在执行过程中会拆分为一下几步:
- JVM会为 instance 分配内存地址以及内存空间。
- 在执行时通过构造方法实例化对象。
- 将 instance 指向在第一步分配好的内存地址 。
根据我们前面重排序的知识,以上代码在真正执行时可能是 1、2、3,也可能是 1、3、2。
如果在多线程环境下,使用1、3、2可能会出现问题:
假设线程A刚执行了1、3步骤,但还没有执行2,此时 instance 已经指向了JVM分配的内存地址。如果现在线程B进入 if(instance == null) ,会直接拿到 instance 的对象(此instance是刚才线程A并没有new的对象)。这时拿到的 instance 对象是null,如果直接使用必然会报错!
解决方案就是添加 Volatile 关键字来禁止 程序使用1、3、2的重排序顺序。
public volatile static Singleton instance = null;
Volatile 是否能保证变量的原子性、 线程安全
不能!
下面通过一段代码来验证一下:
/**
* @author leizige
*/
public class TestVolatile {
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
/**
* 每个线程num++300次,100个线程在线程安全时,结果应该为300w;
*/
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 30000; j++) {
/* 不是一个原子性操作 */
num++;
}
}).start();
}
/**
* 这里不能直接打印num,代码当中开了100个线程,main也是一个线程。
* 假如100个线程,每个线程执行需要5ms
* 但是从main方法开始到打印num,可能只需要花2ms
* 如果main线程执行完,子线程还没执行完,所以会发生错误
* 所以需要先暂停1s,让子线程执行完
*/
Thread.sleep(1000);
System.err.println(num);
}
}
以上代码执行结果与预期的300w不符,下面我们分析一下线程不安全的原因:
其实造成原因的代码还是 num++,这句代码不是一个原子性操作。
num++ 等价与 num = num +1;
num = num +1 还可以拆分为以下两步:
- num + 1;
- num = 第一步的结果;
假设两个线程在执行时通过执行 num + 1;(假设此时num的值为10)
线程A执行 10 +1 = 11;
线程B执行 10 +1 = 11;
正常执行完线程A和B之后num的值应该为12,在并发环境下可能出现两个线程同时+1,就造成了漏加的情况,所以结果与预期不符合。
如何将 num 变成原子性的呢,只要使用 java.util.concurrent.atomic包下的 AtomicInteger。该类能够保证原子性的核心是因为提供了compareAndSet()方法,该方法提供了 CAS算法(无锁算法)。
/**
* @author leizige
*/
public class TestVolatile {
// private volatile static int num = 0;
private static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
/**
* 每个线程num++300次,100个线程在线程安全时,结果应该为300w;
*/
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 30000; j++) {
// num++
/* 一个原子性操作 */
num.incrementAndGet();
}
}).start();
}
System.err.println(num);
}
}