标签:效率 位置 参数 执行 ali 处理器 内存区域 常见 系统
目录
?? 本文以及示例源码已归档在 javacore
Java 内存模型(Java Memory Model),简称 JMM。
JVM 中试图定义一种 JMM 来屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。
物理内存的第一个问题是:硬件处理效率。
高速缓存解决了 硬件效率问题,但是引入了一个新的问题:缓存一致性(Cache Coherence)。
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。
为了解决缓存一致性问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。
除了高速缓存以外,为了使得处理器内部的运算单元尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化。处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。
乱序执行技术是处理器为提高运算速度而做出违背代码原有顺序的优化。
内存模型
这个概念。我们可以理解为:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的物理计算机可以有不一样的内存模型,JVM 也有自己的内存模型。
JVM 中试图定义一种 Java 内存模型(Java Memory Model, JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序 在各种平台下都能达到一致的内存访问效果。
JMM 的主要目标是 定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数值对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效能,JMM 并没有限制执行引擎使用处理器的特定寄存器或缓存来和主存进行交互,也没有限制即使编译器进行调整代码执行顺序这类优化措施。
JMM 规定了所有的变量都存储在主内存(Main Memory)中。
每条线程还有自己的工作内存(Working Memory),工作内存中保留了该线程使用到的变量的主内存的副本。工作内存是 JMM 的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
说明:
这里说的主内存、工作内存与 Java 内存区域中的堆、栈、方法区等不是同一个层次的内存划分。
类似于物理内存模型面临的问题,JMM 存在以下两个问题:
as-if-serial
属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作。JVM 实现时必须保证下面介绍的每种操作都是 原子的(对于 double 和 long 型的变量来说,load、store、read、和 write 操作在某些平台上允许有例外 )。
lock
(锁定) - 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。unlock
(解锁) - 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。read
(读取) - 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load
动作使用。write
(写入) - 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。load
(载入) - 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。use
(使用) - 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。assign
(赋值) - 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。store
(存储) - 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write
操作使用。如果要把一个变量从主内存中复制到工作内存,就需要按序执行 read
和 load
操作;如果把变量从工作内存中同步回主内存中,就需要按序执行 store
和 write
操作。但 Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
JMM 还规定了上述 8 种基本操作,需要满足以下规则:
上文介绍了 Java 内存交互的 8 种基本操作,它们遵循 Java 内存三大特性:原子性、可见性、有序性。
而这三大特性,归根结底,是为了实现多线程的 数据一致性,使得程序在多线程并发,指令重排序优化的环境中能如预期运行。
原子性即一个操作或者多个操作,要么全部执行(执行的过程不会被任何因素打断),要么就都不执行。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
在 Java 中,为了保证原子性,提供了两个高级的字节码指令 monitorenter
和 monitorexit
。这两个字节码,在 Java 中对应的关键字就是 synchronized
。
因此,在 Java 中可以使用 synchronized
来保证方法和代码块内的操作是原子性的。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
JMM 是通过 "变量修改后将新值同步回主内存, 变量读取前从主内存刷新变量值" 这种依赖主内存作为传递媒介的方式来实现的。
Java 实现多线程可见性的方式有:
volatile
synchronized
final
有序性规则表现在以下两种场景: 线程内和线程间
as-if-serial
)的方式执行,此种方式已经应用于顺序编程语言。synchronized
关键字修饰)以及 volatile
字段的操作仍维持相对有序。在 Java 中,可以使用 synchronized
和 volatile
来保证多线程之间操作的有序性。实现方式有所区别:
volatile
关键字会禁止指令重排序。synchronized
关键字通过互斥保证同一时刻只允许一条线程操作。JMM 为程序中所有的操作定义了一个偏序关系,称之为
先行发生原则(Happens-Before)
。先行发生原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作间是否可能存在冲突的所有问题。
unLock
操作先行发生于后面对同一个锁的 lock
操作。volatile
变量的写操作先行发生于后面对这个变量的读操作。Thread
对象的 start()
方法先行发生于此线程的每个一个动作。Thread.join()
方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行。interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()
方法检测到是否有中断发生。finalize()
方法的开始。Java 中如何保证底层操作的有序性和可见性?可以通过内存屏障。
内存屏障是被插入两个 CPU 指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障有序性的。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障可见性。
举个例子:
Store1;
Store2;
Load1;
StoreLoad; //内存屏障
Store3;
Load2;
Load3;
复制代码
对于上面的一组 CPU 指令(Store 表示写入指令,Load 表示读取指令),StoreLoad 屏障之前的 Store 指令无法与 StoreLoad 屏障之后的 Load 指令进行交换位置,即重排序。但是 StoreLoad 屏障之前和之后的指令是可以互换位置的,即 Store1 可以和 Store2 互换,Load2 可以和 Load3 互换。
常见有 4 种屏障
LoadLoad
屏障 - 对于这样的语句 Load1; LoadLoad; Load2
,在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。StoreStore
屏障 - 对于这样的语句 Store1; StoreStore; Store2
,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。LoadStore
屏障 - 对于这样的语句 Load1; LoadStore; Store2
,在 Store2 及后续写入操作被执行前,保证 Load1 要读取的数据被读取完毕。StoreLoad
屏障 - 对于这样的语句 Store1; StoreLoad; Load2
,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。Java 中对内存屏障的使用在一般的代码中不太容易见到,常见的有 volatile
和 synchronized
关键字修饰的代码块(后面再展开介绍),还可以通过 Unsafe
这个类来使用内存屏障。
volatile
是 JVM 提供的 最轻量级的同步机制。
volatile
的中文意思是不稳定的,易变的,用 volatile
修饰变量是为了保证变量在多线程中的可见性。
volatile
变量具有两种特性:
这里的可见性是指当一条线程修改了 volatile 变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。
线程写 volatile 变量的过程:
线程读 volatile 变量的过程:
注意:保证可见性不等同于 volatile 变量保证并发操作的安全性
在不符合以下两点的场景中,仍然要通过枷锁来保证原子性:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他状态变量共同参与不变约束。
但是如果多个线程同时把更新后的变量值同时刷新回主内存,可能导致得到的值不是预期结果:
举个例子: 定义 volatile int count = 0
,2 个线程同时执行 count++ 操作,每个线程都执行 500 次,最终结果小于 1000,原因是每个线程执行 count++ 需要以下 3 个步骤:
具体一点解释,禁止重排序的规则如下:
volatile
变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;volatile
变量访问的语句放在其后面执行,也不能把 volatile
变量后面的语句放到其前面执行。普通的变量仅仅会保证该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证赋值操作的顺序与程序代码中的执行顺序一致。
举个例子:
volatile boolean initialized = false;
// 下面代码线程A中执行
// 读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用
doSomethingReadConfg();
initialized = true;
// 下面代码线程B中执行
// 等待initialized 为true,代表线程A已经把配置信息初始化完成
while (!initialized) {
sleep();
}
// 使用线程A初始化好的配置信息
doSomethingWithConfig();
复制代码
上面代码中如果定义 initialized 变量时没有使用 volatile 修饰,就有可能会由于指令重排序的优化,导致线程 A 中最后一句代码 "initialized = true" 在 “doSomethingReadConfg()” 之前被执行,这样会导致线程 B 中使用配置信息的代码就可能出现错误,而 volatile 关键字就禁止重排序的语义可以避免此类情况发生。
具体实现方式是在编译期生成字节码时,会在指令序列中增加内存屏障来保证,下面是基于保守策略的 JMM 内存屏障插入策略:
总结起来,就是“一次写入,到处读取”,某一线程负责更新变量,其他线程只读取变量(不更新变量),并根据变量的新值执行相应逻辑。例如状态标志位更新,观察者模型变量值发布。
JMM 要求 lock、unlock、read、load、assign、use、store、write 这 8 种操作都具有原子性,但是对于 64 位的数据类型(long 和 double),在模型中特别定义相对宽松的规定:允许虚拟机将没有被 volatile
修饰的 64 位数据的读写操作分为 2 次 32 位的操作来进行,即允许虚拟机可选择不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。由于这种非原子性,有可能导致其他线程读到同步未完成的“32 位的半个变量”的值。
不过实际开发中,Java 内存模型强烈建议虚拟机把 64 位数据的读写实现为具有原子性,目前各种平台下的商用虚拟机都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要把用到的 long 和 double 变量专门声明为 volatile。
我们知道,final 成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。 final 关键字的可见性是指:被 final 修饰的字段在声明时或者构造器中,一旦初始化完成,那么在其他线程无须同步就能正确看见 final 字段的值。这是因为一旦初始化完成,final 变量的值立刻回写到主内存。
标签:效率 位置 参数 执行 ali 处理器 内存区域 常见 系统
原文地址:https://www.cnblogs.com/aimei/p/12200389.html