1.硬件层面认识CPU读写
计算机中cpu读写内存数据时,首先会读取 L1 缓存,L1 缓存中读取不到去 L2 缓存读取,读取不到再去 L3 读取,还是读取不到时,再去主内存中读取。cpu 之所以这样做,是因为 cpu 的运行速度远超主内存的读写速度,为了不让主内存的性能限制 cpu 的运行速度,所以加了三级缓存设备,在运行速度上:L1 > L2 > L3 > 主内存。寄存器属于cpu计算时的一个暂时存储中转空间。
所以多线程运行时,每个 cpu 会运行一个线程任务,如果有个共享变量 a=0 存储在主内存中,两个线程同时运行读取共享变量,并且累加共享变量 +1 的操作,这个共享变量就会出现安全问题。因为:每个线程首先读取共享变量 a 的副本到缓存,每个线程累加时都会在副本 a=0 上做累加计算,那么结果有可能就是 1。与期望的结果 2 不相等。
所以为了解决这个问题,主流的解决方式有两种:
1. 通过总线加锁。
早期的cpu时代,给总线加锁,相当于一种悲观锁,会降低多线程的性能。给总线加锁后,同一时刻只允许一个 cpu 运行,当运行中的cpu释放总线锁后,才能运行其它cpu。显然影响cpu的性能。
2. 通过缓存一致性协议。
MESI是缓存一致性协议的一种实现。它的思想是保证缓存中的副本变量都是一致的。也就是说,当其中一个副本被修改时,那么这个线程会设置其它cpu缓存中的 cache line(加载到缓存的最小数据单位) 为无效状态,并刷新修改后的变量到主内存中。当其它 cpu 读取自己缓存中的副本变量时,发现 cache line 为无效状态,就只能去主内存中读取变量。
2. java的内存模型(Java Memory Mode)
java的内存模型指定了 Java 虚拟机如何与计算机的主存进行工作。
java的内存模型是一个抽象的概念,它的不同内存区域映射到计算机硬件结构。如线程的栈空间可能涵盖了硬件上的主内存、缓存、寄存器。堆内存可能包含主内存等。java中规定了内存工作的模型规范描述如下:
1. 共享变量存储于主内存中,每个线程都可以访问。
2. 每个线程都有私有的线程内存或者称为本地内存。
3. 工作内存只存储该线程对共享变量的副本。
4. 线程不能直接操作主内存,只有先操作了工作内存之后才能操作主内存。
5. 工作内存和java内存模型一样也是一个抽象的概念,它其实并不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等。
3. java中通过volatile关键字保证共享变量多线程运算的安全性。
第 1、2 中的内容讲述了硬件层面和jmm层面的内存模型,那么理解下面的内容就比较容易了。
并发三个重要的特性是:
1. 原子性。
执行过程中不会被其它线程打断,要么全执行,要么全不能执行。
2. 可见性。
多线程间共享变量被修改后在线程间立即可见。
3. 顺序性。
保证程序执行指令不会被重排序。单线程中,即使发生指令重排序,也不会有什么影响。但是多线程之间,发生指令重排序,就会影响线程的执行结果。
volatile关键字的作用:
1. 保证了不同线程之间对于共享变量的可见性。
如果一个普通的共享变量 a=0 被两个线程同时读取,那么在这两个线程各自的私有工作空间中,会保存有 a 这个副本变量,a 被其中一个线程修改后,不会让其它线程读取到的。加了volatile关键字修饰后,不管哪个线程修改了 a 这个变量,其它线程工作空间中如果也存有这个 a 变量的副本,那么会立即读取到这个变量的最新值。
2. 禁止了指令重排序操作。
直接禁止了jvm和处理器对 volatile 关键字修饰的指令重排序。
3. 不能保证原子性。
如 i=0; i++ 操作。如果 i 被 volatile 修饰,i++ 在jvm指令中并不是一步操作。大致操作如下:
3.1 cpu 在进行 i++ 时,首先 get i 到缓存。
3.2 在工作空间中给 i +1。
3.3 将 i+1 的结果写入缓存,再刷新到主内存。
可以看出,以上 i++ 的三步操作并不是原子性的,也就是说,在执行三步操作的过程中,这个线程很有可能被其它线程打断而暂停,其它线程可能会读取到没有 +1 之前的变量去操作,这个时候数据的修改就出现不安全的问题了。所以volital修饰的变量并不能保证原子性。
volatile的原理(以下为《java高并发编程详解》一书中的描述):
在OpenJDK源码的 unsafe.cpp 中,会发现关键字volatile修饰过的变量存在一个 “lock;” 的前缀,源码如下:
"lock;" 前缀实际上相当于一个内存屏障,该内存屏障会为指令的执行提供如下几个保障。
1. 确保指令重排时不会将其后面的代码排到内存屏障之前。
2. 确保指令重排时不会将其前面的代码排到内存屏障之后。
3. 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成。
4. 强制将线程工作内存中值的修改刷新到主内存中。
5. 如果是写操作,则会导致其它线程工作内存(CPU Cache)中缓存数据失效。
happens-before规则关于volatile的变量规则
“对于一个volatile域的写,happens-before于任意后续对这个volatile域的读。” 意思是:如果一个共享变量被volatile修改,那么后续任意线程对这个变量的读取必须发生在对这个变量的修改之后。
综上所述:经过volatile修饰的变量,能保证线程间变量的可见性,能保证指令的有序性,不能保证原子性。所以在多线程的应用中,如果想通过volatile修饰变量来保证一个变量的更新操作的数据安全是不可行的,如果想通过volatile修饰的变量来作为多个线程之间的一个可见信号是可行的,如果想通过volatile修饰的变量来保证多线程之间的读写操作不乱序读是可行的。
以下文章是一个 volatile 在 “Double-check单例模式” 中的一个应用:https://blog.csdn.net/jll126/article/details/117636778