Java内存模型
在前面阅读了Thread类的源码,总结了线程的创建、停止、生命周期,以及多线程编程的异常处理,那么接下来就要总结多线程编程下的安全问题。
事实上,并不是所用的情况用多线程就一定比单线程快。多线程如果使用不当,不仅会带来严重的安全性问题,也会造成性能问题。
在JVM内存结构中,我们知道栈、程序计数器、局部变量这些是线程私有的,但是进程范围内的资源,同一个进程内的线程都会共享进程内的地址空间,那么它们会共享堆上分配的对象。当多个线程并发运行时,它们就可能会访问或者修改其他线程正在使用的变量造成严重的后果。那么这些问题是怎么造成的呢?
Java内存模型
Java内存模型,全称Java Memory Model,JMM,目的是为了屏蔽掉各种硬件与操作系统的内存访问差异,让Java程序在各种平台下都能达到一致的访问效果。它是Java虚拟机定义的一组规范,目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
Java内存模型定义了所有的变量都存储在主内存中,每条线程也有自己的工作内存,线程的工作内存中,保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都在工作内存中进行,而不能直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间的变量值的传递均需要通过主内存完成。
CPU有多级缓存,Java做为高级语言,屏蔽了CPU cache的底层细节,用JMM定义了一套读写内存的规范:主内存与工作内存就是JMM抽象出来的概念
线程、主内存、工作内存三者的交互关系如下图所示:
内存之间的交互
那么主内存与工作内存直接具体是怎么交互的呢,一个变量是怎么从主内存拷贝到工作内存的?
主内存与工作内存交互的八种操作
Java内存模型中,定义了以下这8种操作,并规定了虚拟机实现这八种操作时,每一种操作都是原子的、不可再分的:
- lock
作用于主内存的变量,它把一个变量标识为一条线程独占的状态
- unlock
作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read
作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后续的load使用
- load
作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use
作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令就会执行这个操作
- assign
作用于工作内存的变量,它把一个从执行引擎接受到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
- store
作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后序的write操作使用
- write
作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入到主内存的变量中
个人理解,这八个指令其实就是保证了变量从主内存到工作内存的读取/写入,从工作内存到虚拟机使用的读取/写入,每个步骤的原子性。但是在read与load、store和write之间,Java内存模型只要求它们必须按顺序执行,却不要求是连续执行。也就是说读取主内存的变量a、b时,很有可能的顺序是:read a、read b、load b、load a。
八种操作需要满足的规则
此外,Java内存模型还规定了在上述八种操作时,必须满足下面的规则:
-
不允许read和load、store与write操作之一单独出现
-
不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变之后必须把该变化同步回主内存
-
不允许一个线程无原因(没发生过assign操作)把数据从线程的工作内存中同步回主内存中
-
一个新的变量只能在主内存中诞生,不允许工作内存中直接使用一个未被初始化的变量(也就是说,在变量use、store之前必须经过assign与load操作)
-
一个变量在同一时刻只允许一条线程对其进行lock操作,但是lock操作可以被同一条线程执行多次(lock多少次也必须unlock多少次,才能解锁)
-
如果对一个变量执行lock操作,那么会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assign操作初始化变量的值
-
如果一个变量事先没有被lock操作锁定,那么久不允许对它执行unlock操作
-
对一个变量执行unlock操作之前,必须把这个遍历同步回主内存中
上面的描述可能有点绕。
实际上,它们都是为了在并发过程中如何处理原子性、可见性、有序性这三个特征来建立的。
那么原子性、可见性、有序性这三个指的是什么呢?
我们先看下并发编程中非常出名的的两种栗子:read-modify-write和check-and-act。
read-modify-write
大名鼎鼎的read-modify-write问题:(两个线程同时对主内存的一个变量进行读-改-写问题)
测试代码:
@NotThreadSafe
public class NotSafeThreadTest implements Runnable {
public static NotSafeThreadTest instance = new NotSafeThreadTest();
int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instanc