Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与java编程中的变量是有所区别的,它包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量,是从主内存拷贝的副本。线程对内存的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接操作主内存,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递都需要通过主内存来完成,线程、主内存、工作内存之间的关系如图所示:
内存间交互
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的细节,Java内存模型定义了以下8种操作来完成,它们都是原子操作(除了对long和double类型的变量)
- lock(锁定): 作用于主内存中的变量,它将一个变量标志为一个线程独占的状态
- unlock(解锁): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定
- read(读取): 作用于主内存中的变量,它把一个变量的值从主内存中传递到工作内存,以便进行下一步的load操作
- load(载入): 作用于工作内存中的变量,它把read操作传递来的变量值放到工作内存中的变量副本中
- use(使用): 作用于工作内存中的变量,这个操作把变量副本中的值传递给执行引擎。当执行需要使用到变量值的字节码指令的时候就会执行这个操作
- assign(赋值): 作用于工作内存中的变量,接收执行引擎传递过来的值,将其赋给工作内存中的变量。当执行赋值的字节码指令的时候就会执行这个操作
- store(存储): 作用于工作内存中的变量,它把工作内存中的值传递到主内存中来,以便进行下一步write操作
- write(写入): 作用于主内存中的变量,它把store传递过来的值放到主内存的变量中
如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序的执行store和write操作。注意,java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read和write之间、store和write之间是可插入其他指令的。Java内存模型还规定了在执行上述8种基本操作时必须满足以下规则
- 不允许read和load、store和write操作之一单独出现。即不允许一个变量从主内存被读取了,但是工作内存不接受,或者从工作内存回写了但是主内存不接受
- 不允许一个线程丢弃它最近的一个assign操作,即变量在工作内存被更改后必须同步改更改回主内存
- 工作内存中的变量在没有执行过assign操作时,不允许无意义的同步回主内存
- 在执行use前必须已执行load,在执行store前必须已执行assign
- 一个变量在同一时刻只允许一个线程对其执行lock操作,一个线程可以对同一个变量执行多次lock,但必须执行相同次数的unlock操作才可解锁
- 一个线程在lock一个变量的时候,将会清空工作内存中的此变量的值,执行引擎在use前必须重新read和load
- 线程不允许unlock其他线程的lock操作。并且unlock操作必须是在本线程的lock操作之后
在执行unlock之前,必须首先执行了store和write操作
理解JMM中的happens-before 原则
倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下
程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行
- 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)
- volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值
- 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
- 传递性 A先于B ,B先于C 那么A必然先于C
- 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见
- 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断
- 对象终结规则 对象的构造函数执行,结束先于finalize()方法
