Java内存模型
Java线程之间的通信对程序员完全透明,内存可见性问题困扰程序员。
Java内存模型是一种抽象的规范,为了解决多线程对共享数据的读写一致性问题。通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证
并发编程的2个关键问题:
1、线程之间如何通信
2、线程之间如何同步
线程之间的通信机制有两种:共享内存和消息传递
Java线程之间的通信由Java内存模型(JMM)控制。JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看JMM定义了线程与主内存之间的抽象关系。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存。本地内存中存储了该线程读写共享变量的副本。本地内存是JMM抽象概念,并不真实存在。
线程AB之间如要通信的话,必须经过2个步骤
1、线程A把本地内存中的共享变量刷新到主内存。
2、线程B从主内存中去读取线程A之前更新过的共享变量
线程之间通信示意图
指令序列重排序
编译器和处理器为了提高性能会对指令做重排序。重排序分为三种
1、编译器重排序
2、指令并行重排序
3、内存系统重排序
对于处理器重排序,JMM处理器重排序规则会要求JAVA编译器在生成指令序列时,插入内存屏障(Memory Barriers)指令来禁止处理器重排序
JMM属于语言级内存模型,确保在不同的编译器和处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证
并发编程模型分类
处理器使用缓冲区临时保存像内存中写入的数据。因为处理器的运算速度远远大于内存,如果一直向内存中写入数据,会导致cpu等待内存的情况发生。
但是每个处理器的写缓冲区,只对自己所在的处理器可见。这会导致变量在缓冲区时,其他处理器从主存中读取的数据并不是最新数据。而导致程序没有发生预期的结果。因此处理器都会允许对写-读操作做重排序
为保证内存可见性,Java编译器会在生成指令序列的适当位置插入内存屏障来禁止处理器重排序。JMM把内存屏障分为4类:
StoreLoad 是全能型屏障,同时具有其他3个屏障的效果。开销很大,因为需要把写缓冲区的所有数据全部刷新回主内存
happens-before
happens-before 用来阐述操作之间的内存可见性。在JMM中如果一个操作执行的结果需要对另一个操作可见,那么两个操作之间必须存在happens-before关系。可以是一个线程也可以是不同线程。
规则:
程序顺序规则:一个线程的每一个操作,happens-before与该线程的后续任意操作。
监视器锁规则:对于一个锁的解锁,happens-before任意后续对这个volatile域的读。
volatile变量规则:对一个volatile域的写,happens-before任意后续对这个volatile域的读。
传递性:a happens-before b,b happens-before c,那么a happens-before c
重排序
编译器和处理器为了优化程序性能对指令序列进行重新排序。
数据依赖性
处理器和编译器在重排序时,会遵守数据依赖性,不会改变存在数据依赖关系的2个操作的执行顺序。
as-if-serial语义
不管怎么重排序,单线程执行结果不能被改变。
volatile内存语义
volatile的特性:对于一个volatile变量的读写操作,相当于对一个普通变量读写加锁操作。
读一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
volatile变量自身具有下列特性:
1、可见性。对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入
2、原子性。对于任意单个volatile变量读写具有原子性。
volatile写-读内存语义
当写一个volatile变量时,JMM会把该线程对应本地内存共享变量刷新到主内存中。
volatile读内存语义:
当读一个volatile变量时,JMM会把该线程对应的本地内存置位无效。线程接下来从主内存中读取共享内存的值。
volatile内存语义的实现
volatile重排序规则表在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
final域的内存定义
对于final域,编译器和处理器要遵守两个重排序规则。
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
网友评论