Java内存模型解析
定义
并发编程中,需要处理两个关键问题:
- 线程之间如何通信
- 线程之间如何同步
通信指线程之间以何种机制来交换信息,线程之间的通信机制有两种:共享内存和消息传递。
同步指程序中用于控制不同线程间操作发生相对顺序的机制。
Java采用的是共享内存模型,Java线程之间的通信由Java内存模型(JMM)控制。Java内存模型的主要目的是定义程序中各种变量的访问规则。
主内存和本地内存
JMM规定了线程之间共享变量存储在主内存中,每个线程都有私有的本地内存,本地内存存储了共享变量的副本,Java内存模型的示意图如图所示:
从图来看,线程A和线程B之间要通信的话,会进行以下操作:
- 线程A把本地内存中更新过的共享变量刷新到主内存中。
- 线程B去主内存中读取线程A之前更新的变量。
内存间的交互操作
关于如何将一个变量从主内存拷贝到本地内存中,JMM定义了以下八种操作来完成,JVM必须保证每种操作是原子性的。
- lock:作用于主内存的变量,将一个变量标识为一个线程独占状态。
- unlock:作用于主内存的变量,将处于线程独占状态的变量释放出来。
- read:作用于主内存的变量,将一个变量的值从主内存传输到线程的本地内存中。
- load:作用于本地内存的变量,将read操作得到的变量放入本地内存的变量副本中。
- use:作用于本地内存的变量,将本地内存的一个变量值传递给执行引擎。
- assign:作用于本地内存的变量,它把一个从执行引擎接收到的值赋值给本地内存中的变量。
- store:作用于本地内存的变量,将本地内存的值传送到主内存中
- write:作用于主内存的变量,将store操作得到的变量值放入主内存的变量中。
重排序
重排序时指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,重排序分为三种类型:
- 编译器优化的重排序
- 指令并行的重排序,处理器使用指令级并行技术来将多条指令重叠执行。
- 内存系统的重排序,由于处理器使用了缓存技术和读/写缓冲区技术。
重排序会导致多线程程序出现内存可见性问题,对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器,JMM要求编译器生成指令序列的时候,插入内存屏障指令来禁止重排序。
数据依赖性
如果两个操作访问同一个变量,且两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖性分三种类型,如下表所示:
名称 | 代码实例 |
---|---|
写后读 | a = 1; b = a; |
写后写 | a = 1; a = 2; |
读后写 | a = b; b = 1; |
上述现象如果执行顺序发生改变,执行结果就会被改变。
编译器和处理器在重排序时,会遵守数据依赖性原则,不会改变存在依赖关系的两个操作的执行顺序。
Happens-Before
JSR-133使用Happens-Before
的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在Happens-Before
关系。A Happens-Before
B 意味着:A操作的结果对B是可见的。
通俗而言,即:A运行完成后数据结果,B都能读取到。
Happens-Before
原则如下:
- 程序顺序规则:在一个线程内,在程序前面的操作先行发生于后面的操作。
- 管程锁规则:一个
unlock
操作先行发生于后面对同一个锁的lock
操作。 - volatile变量规则:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读。
- 传递性
- 线程
start()
规则:start()
方法调用先行发生于此线程的每一个动作。 - 线程
join()
规则:线程的结束先行发生于join()
方法返回。 - 线程
interrupt()
规则:对线程的interrupt()
方法的调用先行发生于被中断线程代码检测到中断事件的发生 - 对象终结规则:一个对象的初始化完成先行发生于它的
finalize()
方法的开始。
线程安全
共享资源的安全程度按照强弱顺序分为以下五类:
- 不可变(Immutable):一定是线程安全,不需要任何措施进行保护。
- final修饰的变量
- String
- 枚举
- Number部分子类:Long,Double,BigInteger,BigDecimal
- 绝对线程安全:不需要做任何额外的同步措施。
- 相对线程安全:需要保证对这个对象的单独的操作是线程安全的,不需要做额外的保障措施。但是一些特定顺序的连续的调用,需要做同步措施。
- Java中大部分线程安全类属于该类,Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
- 线程兼容:指对象本身并不是现成安全的,但是可以通过在调用端正确使用同步手段来保证对象在并发环境中可以安全的使用。
- Java中大部分类属于线程兼容的,例如:ArrayList、HashMap。
- 线程对立:无法通过同步手段实现线程安全。