1 什么是指令重排?
Java 内存模型允许编译器和处理器对指令重排序以提高运行性能, 并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。
int i = 1; //(1)
int j = 2; //(2)
int k = i + j; //(3)
如上代码,变量K的值依赖 i 和 j 的值,所以重排序后能操作 (3) 的操作在 1,2之后。1和2谁先执行就不一定了,不过在单线程中不会有什么问题,下面看一个多线程的例子:
package com.example.demo.thread;
/**
* @author wb-hll364276
* @date 2020/5/9.
*/
public class Demo3 {
private static int num = 0;
private static Boolean ready = false;
static Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
if(ready){ (1)
System.out.println(num + num); (2)
}
System.out.println("thread111111");
}
}
});
static Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
num = 2; (3)
ready =true; (4)
System.out.println("thread222222");
}
});
public static void main(String[] args) throws InterruptedException {
thread1.start();
thread2.start();
Thread.sleep(10);
thread1.interrupt();
System.out.println("over");
}
}
首先这段代码里面的变量没有被声明为volatile的,也没有使用任何同步措施, 所以在多线程下存在共享变量内存可见性问题。这里先不谈内存可见性问题,因为通过把变量声明为volatile 的本身就可以避免指令重排序问题。
这里先看看指令重排序会造成什么影响,如上代码在不考虑、内存可见性问题的情况下一定会输出4 ? 答案是不一定,由于代码(1) (2) (3) (4)之间不存在依赖关系, 所以写线程的代码(3) ( 4 )可能被重排序为先执行(4)再执行(3), 那么执行( 4 )后, 读线程可能已经执行了(1)操作, 并且在(3)执行前开始执行(2)操作, 这时候输出结果为0而不是4。
重排序在多线程下会导致非预期的程序执行结果,而使用volatile 修饰ready 就可以避免重排序和内存可见性问题。
还有我们熟悉的双端检锁的单例模式也不一定线程安全,原因是可能会指令重排,所以我们在定义单例对象的时候加volatile关键字。
class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingleton(); //创建单例实例
}
}
}
return instance;
}
}
以上就是使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,如果你使用的JDK版本是 1.5及以上版本可以不用加volatile。volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。那什么是最好的实现单例模式的方式呢? 在这留个小问题~
2 volatile为什么能禁止指令重排 ?
我们先来了解一下happen-before规则
虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排做出一些规则限制,并不能让所有的指令都随意的改变执行位置,主要有以下几点:
- 单线程每个操作,happen-before于该线程中任意后续操作
- volatile写happen-before与后续对这个变量的读
- synchronized解锁happen-before后续对这个锁的加锁
- final变量的写happen-before于final域对象的读,happen-before后续对final变量的读
- 传递性规则,A先于B,B先于C,那么A一定先于C发生
内存屏障
内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。使用volatile关键字就会插入一个内存屏障
内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。这是不是就是所谓的可见性~