1. 并发解决了什么问题
-
多任务处理。
-
在处理器(CPU)运算速度 与 存储设备 (内存)、通讯设备 的速度差距极大的前提下,更有效的利用 处理器 的运算能力。
##2. 计算机内存模型
-
处理器
-
高速缓存
-
主内存
##3. JVM 内存模型 - JMM
-
java 线程
-
线程工作内存
-
主内存
##4. Java 中的 内存一致性交互操作
-
lock :
作用于主内存 变量,将变量标记为 线程 独占
-
unlock :
作用于主内存 变量, 释放线程独占状态
-
read :
作用于主内存 变量,将变量值从主内存传输到工作内存
-
load :
作用于工作内存 变量,将read 之后的变量值放到工作内存的变量副本
-
use :
作用于工作内存变量, 将 工作内存中的一个变量值传递给执行引擎(线程),当虚拟机遇到一个需要使用变量的字节码指令,就会执行这个操作
-
assign :
作用于工作内存的变量, 把 执行引擎的变量值赋值给工作内存的变量,当虚拟机遇到一个赋值变量的字节码指令,就会执行这个操作
-
store :
作用于工作内存变量,把工作内存中的一个变量值传输到主内存
-
write :
作用于 主内存变量,把store传输过来的变量回写到主内存
##4. Java 中的 内存一致性保证
[read,load] 、 [store,write] 必须成对出现,也就是不允许 从主内存中读取了数据工作内存不接受,或工作内存数据传输到主内存,主内存不回写。
不允许线程丢弃(回滚) assign 操作,也就是 变量在工作内存中改变之后,必须同步到主内存
不允许线程把数据 在没有 assign 之前就同步回主内存
变量只能在主内存中诞生,也就是不能将未进行 load ,assign 的变量进行 use 和 store ,也就是 load -> use; assign -> store 必须被保证
一个变量在同一时刻只接受一个线程的lock操作,注意可以接受同一线程的多次lock操作,只需要执行同样数目的 unlock操作就能释放锁
如果对变量执行了lock操作,将会清空工作内存中的这个变量,再次使用之前,必须重新执行 load 或者 assign 来初始或变量
如果一个变量没有被lock,则不允许对它执行unlock,也不允许一个线程 unlock 被其他线程lock 的变量
一个变量在unlock之前,必须把此变量同步回写到主内存,也就是 store-> write -> unlock
##4. JMM 对 volatile 变量的特殊规则
-
volatile 变量的特征
volatile 类型的变量具备 **可见性** ,也就是 一个线程修改了这个变量,新值对于其他线程是立即得知的(原理是volatile 变量每次被使用之前,必须要从主内存中同步刷新;对volatile变量的 修改 操作,也必须立即回写到主内存)
volatile 的变量禁止解释器进行指令重排优化
volatile 变量保证,即使对于 long和double 64位类型的数据,也不允许将其读写操作分为2次 32位的操作来进行(这里是JVM规范中的宽松要求,后面会细谈)
-
volatile 变量的可见性 == 同步?
volatile 可见性 != 同步
volatile 保证线程任意时刻拿到的数值是最新的,但是多个线程都要修改volatile变量的情况,从线程拿到变量数值开始,到对变量的重新赋值操作结束,这段时间是不会再从主内存同步的,这样其他线程如果修改了这个volatile变量,就会导致数据的不同步,其实主要原因就是,对变量的修改,赋值的操作并不是原子性的一步操作,而是需要多步操作,这样就导致了数据的异常
-
volatile 变量的使用场景
所有线程之中的运算结果,不依赖volatile变量的当前值,也就是说volatile 变量不参与到实际的运算;这里有个例外,如果只有一个线程可以修改volatile的数值,那么volatile也可以参与线程的结果运算,因为没有多个线程共同修改,就不会有数据异常
在条件判断中,变量不需要与其他的状态变量共同参与不变约束,也就是说 if(volatileValue && anotherBooleanValue) 这样的情况是不被允许的,因为条件判断的过程也不是原子性的,在条件判断的过程中,很可能volatile变量的数值已经被更改了,只有单独使用volatile变量作为条件判断的条件语句才可以被多线程使用。
-
指令重排优化
jvm 解释器 会对方法中的代码进行字节码级别的优化,来提高执行效率,可能会导致,代码实际执行的顺序,并不是源代码中的书写顺序;
指令重排优化,会保证,所有依赖某个变量赋值结果的地方,会得到正确的结果,也就是说: int a = intValueB; 这样的情况是会保证intValueB之前的操作会发生在对intValueB的使用之前,但是如果,一个变量在一段代码中没有参与到其他变量的赋值之中,也就是说这样的例子:
public class ResourceCheckHandler{
public boolean isCheck = false;
public void checkResource(){
// check ... isCheck = true;
}
public static void main(String[] args){
final ResourceCheckHandler rcHandler = new ResourceCheckHandler(); new Thread(new Runnable{ public void run(){ while(rcHandler.isCheck){ // turn to next handler ... } }}).start(); }}
注意这里,checkResource()方法中,isCheck 只是在方法结尾赋值为true,它的数值在这个方法中没有影响到其他变量的数值,那么这句代码的执行顺序,就有可能在指令重排优化的时候被提前到其他代码的前面,导致这个方法还没有执行结束,实际上isCheck变量就已经是 true 了。 解决的方法就是,把isCheck 定义为volatile,那么解释器在进行指令重排优化的时候就不会把isCheck进行重排了。
-
long/double 数据的JVM 宽松协定
JVM规范中,允许JVM的某个实现,选择对 64位数据类型的 load,store,read,write 不保证其原子性,可以分为2次对32位数据的读写过程,也就是 double 和 long的非原子协定(Nonatomaic Treatment of double and long Variables)
对于市面上的商用虚拟机,均选择把 64 位数据的读写一次原子操作实现,因而这种读写半个变量的情况基本不会发生,因此,对于 long、double的类型,不必每个变量都加上volatile的关键字修饰