一、缓存一致性
一个简单的计算机可以抽象为CPU、内存及I/O设备,CPU处理与运算数据,最后写入到内存中。CPU运行首先会从内存中取出运算指令进行执行,最后写回内存。
CPU的运算速度远高于内存,现代计算机在CPU与内存中加了一层或多层“高速缓存”,高速缓存的速度与CPU相当。
当多个处理器的运算任务都涉及到同一块主内存区域时,将可能导致各自缓存数据不一致的问题。
为了解决这个问题,要求CPU在读写缓存时遵循“缓存一致性协议”。
二、Java内存模型(JMM)
Java Memory Model是一个抽象的定义,简单理解为线程访问共享变量的方式。
JMM规定所有变量都存储在主内存中,每条线程都有自己的工作内存。线程的工作内存中保存了被线程使用的变量的主内存副本,对变量的操作都在工作内存中进行。不同线程之间变量值的传递只能通过主内存来完成。
Java线程之间的通信采用的是共享内存。
当多个线程都涉及到同一块主内存区域时,将可能导致各自缓存数据不一致的问题。
三、volatile
1.并发三大特性
- 原子性:原子在化学中反应上是不可在分割的粒子。因此原子性指的是一个不可以被分割的操作,即这个操作在执行过程中不能被中断,要么全部不执行,要么全部执行。且一旦开始执行,不会被其他线程打断。
- 有序性:指程序按照代码的先后顺序执行。有时候为了优化性能,编译器会对字节码指令进行重排序。但是能保证重排序后的执行结果与重排序之前是一致的。
- 可见性:指的是一个线程修改了共享变量后,另外线程能立即感知这个变量被修改。
volatile在并发编程中,它的作用是保证:可见性、有序性。
2.volatile的可见性
当两个线程共用一个共享变量时,如果其中一个线程修改了这个共享变量的值。但是由于另外一个线程在自己的工作内存中已经保留了一份该共享变量的副本,因此它无法感知该变量的值已经被修改。
例如:
public class VolatileDemo implements Runnable{
private static boolean READY;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new VolatileDemo(),"Thread 1");
t1.start();
Thread.sleep(1000);
READY = true;
System.out.println("ready = " + READY);
Thread.sleep(5000);
System.out.println("main thread is end.");
}
@Override
public void run() {
while(!READY)
System.out.println(Thread.currentThread().getName()+"线程结束");
}
}
代码中成员变量READY,默认值false,进入t1线程死循环。主线程修改READY,睡眠后t1仍未结束,睡眠结束,主线程结束。
运行结果:
ready = true
main thread is end.
3.volatile的有序性
编译器为了优化程序性能,可能会在编译时对字节码指令进行重排序。在多线程中,重排序后的代码则可能会出现问题。volatile关键字就可以禁止编译器对字节码进行重排序(内存屏障)。
例如:
public class DoubleCheckLockDemo{
private static volatile DoubleCheckLockDemo INSTANCE;
private DoubleCheckLockDemo(){}
public static DoubleCheckLockDemo getINSTANCE(){
if(null == INSTANCE){
synchronized (DoubleCheckLockDemo.class){
if(null == INSTANCE) {
INSTANCE = new DoubleCheckLockDemo();
}
}
}
return INSTANCE;
}
}
如果上述代码中没有给INSTANCE加上volatile关键字会怎么呢?
首先INSTANCE = new DoubleCheckLock();并不是一个原子操作,实例化对象的字节指令可以分为三步,如下:
- 分配对象内存:memory = allocate();
- 初始化对象:INSTANCE(memory);
- INSTANCE指向刚分配的内存地址:INSTANCE= memory;
而由于编译器的指令重排序,以上指令可能会出现以下顺序:
- 分配对象内存:memory = allocate();
- INSTANCE指向刚分配的内存地址:INSTANCE= memory;
- 初始化对象:INSTANCE(memory);
如果线程1第一次调用单例方法,在该线程的时间片轮转结束后执行到了优化后的第二个指令,即instance被赋值,但是还未被分配初始化对象。
此时,线程2抢到了CPU时间片,同时调用了getInstance方法,第一次校验就发现instance不为null,遂将其返回。在得到这个单例后调用单例的方法,此时必定出现空指针异常。
此时,我们便可以通过volatile关键字来禁止编译器的优化,从而避免空指针的出现。
四、内存屏障
参考:(16条消息) Volatile如何保证有序性(禁止指令重排)_volatile怎么防止指令重排序_heaven殇灬的博客-CSDN博客
作用:
- 保证特定操作的执行顺序;
- 保证某些变量的内存可见性(volatile的内存可见性是利用该特性实现的)。
由于编译器和处理器都能执行指令重排优化,如果在指令之间插入一条内存屏障则会告诉编译器和cup不管在任何情况下,无论任何指令都不能和这条内存屏障进行指令重排,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障的另外一个作用就是强制刷出各种CPU的缓存数据,因此在任何CPU上的线程都能读取到这些数据的最新值。