◆
JMM 的基本概念
◆
Java 作为平台无关性语言, JLS(Java 语言规范)定义了一个统一的内存管理模型 JMM(Java Memory Model).JMM 规定了 jvm 内存分为主内存和工作内存 , 主内存存放程序中所有的类实例, 静态数据等变量, 是多个线程共享的, 而工作内存存放的是该线程从主内存中拷贝过来的变量以及访问方法所取得的局部变量, 是每个线程私有的其他线程不能访问. 每个线程对变量的操作都是以先从主内存将其拷贝到工作内存再对其进行操作的方式进行, 多个线程之间不能直接互相传递数据通信, 只能通过共享变量来进行.
从上图来看, 线程 1 与线程 2 之间如要通信的话, 必须要经历下面 2 个步骤:
首先, 线程 1 把本地工作内存中更新过的共享变量刷新到主内存中去.
然后, 线程 2 到主内存中去读取线程 1 之前已更新过的共享变量.
典型的高并发引起的问题就存在由于线程读取到的数据还没有从另外的线程刷新到主内存中而引起的数据不一致问题.
◆
主内存与工作内存的数据交互
◆
JLS 一共定义了 8 种操作来完成主内存与线程工作内存的数据交互:
lock: 把主内存变量标识为一条线程独占, 此时不允许其他线程对此变量进行读写
unlock: 解锁一个主内存变量
read: 把一个主内存变量值读入到线程的工作内存
load: 把 read 到变量值保存到线程工作内存中作为变量副本
use: 线程执行期间, 把工作内存中的变量值传给字节码执行引擎
assign: 字节码执行引擎把运算结果传回工作内存, 赋值给工作内存中的结果变量
store: 把工作内存中的变量值传送到主内存
write: 把 store 传送进来的变量值写入主内存的变量中
使用标准的操作再来重现一下上方的 2 个线程之间的交互流程则是这样的:
线程 1 从主内存 read 一个值为 0 的变量 x 到工作内存
使用 load 把变量 x 保存到工作内存作为变量副本
将变量副本 x 使用 use 传递给字节码执行引擎进行 x++ 操作
字节码执行引擎操作完毕后使用 assign 将结果赋值给变量副本
使用 store 把变量副本传送到主内存
使用 write 把 store 传送的数据写到主内存
线程 2 从主内存 read 到 x, 然后 load->use->assign->store->write
另外使用这 8 种操作也有一些规则:
read 和 load 必须以组合的方式出现, 不允许一个变量从主内存读取了但工作内存不接受情况出现
store 和 write 必须以组合的方式出现, 不允许从工作内存发起了存储操作但主内存不接受的情况出现
工作内存的变量如果没有经过 assign 操作, 不允许将此变量同步到主内存中
在 use 操作之前, 必须经过 load 操作
在 store 操作之前, 必须经过 assign 操作
unlock 操作只能作用于被 lock 操作锁定的变量
一个变量被执行了多少次 lock 操作就要执行多少次 unlock 才能解锁
一个变量只能在同一时刻被一条线程进行 lock 操作
执行 lock 操作后, 工作内存的变量的值会被清空, 需要重新执行 load 或 assign 操作初始化变量的值
对一个变量执行 unlock 操作之前, 必须先把此变量同步回主内存中
◆
多线程中的原子性, 可见性, 有序性
◆
原子性: 关于原子性的定义可以参考我的上篇博客《浅谈数据库事务》. 在 JLS 中保证原子性的操作包括 read,load,assign,use,store 和 write. 基本数据类型 (除了 long 和 double) 操作都具有原子性.
如果需要更大范围的原子性操作的时候, 可以使用 lock 和 unlock 操作来完成这种需求.
可见性: 是指当一个线程修改了共享变量的值, 其他线程是否能够立即得知这个修改.
由上方 JMM 的概念得知, 线程操作数据是在工作内存的, 当多个线程操作同一个数据的时候很容易读取到还没有被 write 到主内存变量的值.
Java 是如何保证可见性的: volatile,synchronized,final 关键字
有序性: 在并发时, 程序的执行可能会出现乱序. 给人的直观感觉就是: 写在前面的代码, 会在后面执行. 有序性问题的原因是因为程序在执行时, 可能会进行指令重排, 重排后的指令与原指令的顺序未必一致. 关于指令重排会在下方讲.
◆
指令重排
◆
1234 复制代码 | int a=1;int b=2;int c=3;int d=4; 复制代码 |
你能说出上方这段代码的执行顺序么? 其实我们可能理所当然的以为它会从上往下顺序执行. 事实上, 在实际运行时, 为了优化指令的执行顺序等, 代码指令可能并不是严格按照代码语句顺序执行的. 上方的代码执行顺序可能完全反过来, 这个就是指令重排.
不过呢, 指令重排也不是可以随意重排的, 它需要遵守一定的规则:
程序顺序规则: 一个线程内保证语义的正确性.
锁规则: 解锁肯定先于随后的加锁前.
volatile 规则: 对一个 volatile 的写, 先于 volatile 的读.
传递性: 如果 A 先于 B, 且 B 先于 C, 那么 A 肯定先于 C.
start()规则: 线程的 start()操作先于线程的其他操作.
join()规则: 线程的所有操作先于线程的关闭.
程序中断规则: 线程的中断先于被中断后执行的代码.
对象 finalize 规则: 一个对象的初始化完成先于 finalize()方法.
◆
volatile 关键字
◆
volatile 关键字旨在告诉虚拟机在这个地方要注意不能随意的进行指令重排, 而虚拟机看到一个变量被 volatile 修饰以后就会采用一些特殊的手段来保证变量的可见性. 不过要注意的是 volatile 关键字不能保证原子性.
来源: https://juejin.im/post/5c9437d85188252d856302b0