JVM学习笔记——java内存模型与线程(1)

概述

多任务处理出现的重要原因是计算机的运算速度与存储及通信子系统的速度差距太大,大量的时间花费在磁盘I/O,数据库访问或者数据库访问上。除了充分利用计算机处理器的能力外,一个服务端同时对多个客户端提供服务则是另一个更具体的并发应用场景,对于计算量相同的,程序并发协调的越有条不紊,效率自然就高,反之线程之间频繁的阻塞甚至死锁,将会大大降低程序的并发能力。

硬件的效率以及一致性

由于计算机的处理器的运算速度与存储设备的读写速度存在几个数量级的差距,所以加入高速缓存作为缓冲,虽然这很好的解决了处理器和内存之间的速度矛盾,但也带来了更高的复杂度,带来了一个新问题:缓存一致性。
在多处理器系统中,每个处理器都有自己的高速缓存,但是又共享同一块主内存,当多个处理器的运算任务都涉及同一块主内存区域时,可能导致各自的缓存数据不一致,那么同步回主存,以哪个数据为准?为了解决这个问题,需要处理器在访问主存是遵循一系列协议来进行操作,比如MSI,MESI,MOSI,FireFly,Dragon Protocol等,而标题上的内存模型,可以理解为在特定的操作协议下,对特定的内存或者告诉缓存进行读写访问的过程抽象,不同架构的机器有不同的内存模型,java虚拟机也有自己的内存模型。
JVM学习笔记——java内存模型与线程(1)_第1张图片
除了增加告诉缓存外,处理器会对输入代码进行乱序执行优化以充分利用处理器内部的运算单元,处理器会在计算之后将乱序执行的结果重组,保证该结果与乱序执行的结果是一致的,但不保证该程序中每个语句计算的先后顺序和输入代码的顺序一致,故,如果存在一个计算任务的结果依赖于另一个程序的运算结果,那么顺序并不能依赖于代码的先后顺序来保证,相似的,java虚拟机的即使编译器中也有类似的指令重排序。

java内存模型

java的内存模型是平台无关的,c/c++直接使用物理硬件与操作系统的内存模型,因此会由于不同机器内存模型的不同,程序运行出现各种各样的问题。

主内存与工作内存

java内存模型的主要目标是定义各个变量的访问规则,即虚拟机从内存中取出即将变量放入内存中的底层细节,这里的变量包括了实例字段,静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的。JVM中所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、 赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
JVM学习笔记——java内存模型与线程(1)_第2张图片

内存间交互操作

Java内存模型中定义了以下8种操作来完成,每一种操作都是原子的、 不可再分的。

操作名 作用
lock(锁定) 作用于主内存的变量,它把一个变量标识为一条线程独占的状态
unlock(解锁) 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取) 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入) 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
use(使用) 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎
assign(赋值) 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
store(存储) 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
write(写入) 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

上述8种基本操作时必须遵循一系列规则,比如要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,这样十分繁琐,后面将介绍这种定义的一个等效判断原则——先行发生原则,用来确定一个访问在并发环境下是否安全。

对于volatile型变量的特殊规则

volatile型变量的规则可以参考这篇这篇文章。
值得注意的一点是,基于volatile变量的运算在并发下并不是安全的,volatile只能保证对单次读/写的原子性,如果多个线程同时对某个数据进行读写操作,那就很容易读到脏数据。
不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性:
1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2. 变量不需要与其他的状态变量共同参与不变约束。
使
用volatile变量是禁止指令重排序优化的,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

Map configOptions;
char[]configText;
//此变量必须定义为volatile
volatile boolean initialized=false//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用
configOptions=new HashMap();
configText=readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized=true//假设以下代码在线程B中执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized){
sleep();
}/
/使用线程A中初始化好的配置信息
doSomethingWithConfig();

如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一句的代码“initialized=true”被提前执行。这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况的发生。

对于long和double型变量的特殊规则

对于64位的数据类型(long和double),在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、 store、 read和write这4个操作的原子性,这点就是所谓的long和double的非原子性协定(Nonatomic Treatment of double and long Variables)。
但是各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,所以,仅作了解即可。

原子性、 可见性与有序性

Java内存模型是围绕着在并发过程中如何处理原子性、 可见性和有序性这3个特征来建立的,
原子性:由Java内存模型来直接保证的原子性变量操作包括read、 load、assign、 use、 store和write,基本数据类型的访问读写是具备原子性的,如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,synchronized关键字就隐式的使用这两个操作,因此在synchronized块之间的操作也具备原子性。
可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。普通变量和volatile变量都通
过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,不同点在于,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
Synchronized和final也实现了可见性,前者是因为对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,后者则是由于被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那在其他线程中就能看见final字段的值。
有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。 前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

先行发生原则

先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、 发送了消息、 调用了方法等。
1. Java内存模型存在一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。
2. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行生于书写在后面的操作。
3. 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。 这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
4. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
5. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
6. 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、 Thread.isAlive()的返回值等手段检测到线程已经终止执行。
7. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
8. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于其finalize()方法的开始。
9. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

你可能感兴趣的:(JVM学习笔记)