JMM内存模型
概述
为了保证CPU高速运算与内存速度不匹配问题,引入了缓存的概念,JMM屏蔽了底层细节展现的分为主内存、工作内存、线程。
- 主内存
存储在运行中可被多个线程共享的数据。 - 工作内存
各线程无法直接操作主内存中数据,只能将主内存数据放置工作内存,操作完成后在写回。
内存间交互
规定了主内存和工作内存交互方式,共包括lock、unlock、read、load、use、assign、store、write。
- lock (锁定)
对于锁定操作,在一个线程中锁定时该变量无法在其他线程中获取。 - unlock(取消锁定)
将锁定变量释放锁定,取消锁定后变量值存储到主内存中。 - read和load(读取和载入)
这两个操作必须协同工作,read表示读取某个变量到工作内存,load表示将这个变量放入线程私有的工作内存中对应变量。 - use(使用)
将工作内存中变量值交由执行引擎使用。 - assign(赋值)
将执行引擎中传递回来的值赋值给工作内存中对象变量。 - store和write(存储和写入)
这两个操作必须协同工作,store表示保存某个变量的值传入主内存中,write表示将这个变量的新值刷新至主内存。
Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
- 不允许read和load 或者store和write 两组操作单独出现一个,即读入工作内存中,对应变量值不接受。写入主内存中,对应变量值不接受。
- 不允许读入数据后未更改便写入回主内存
- 不允许一个线程丢弃它最近的assign操作(笔者感觉这里应该是写回操作,如果理解有误还望指正),即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 一个变量使用前需要从主内存中加载,即use之前需要read和load。
- 一个变量只允许由一个线程lock,但是同一个线程可以lock多次,lock和unlock需要一一对应。
- 必须lock过才能unlock,而且不允许unlock非本线程锁定对象。
- unlock之前,需要把修改过变量写入(store和write)到主内存。
volatile关键字
使用该关键字修饰类变量时,可以保证变量的可见性和有序性。
- 使用该关键字修饰变量后,当变量发生赋值会立刻写回主内存中并通知其他线程该值失效。
- 使用该关键字修饰变量后,禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的所谓“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
指令重排序:在不影响程序运行结果的前提下,可以不一定按照编码顺序来执行。
为了满足以上要求,对于volatile有以下要求:
- 在工作内存中,每次使用变量前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量所做的修改。
- 在工作内存中,每次修改变量后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量所做的修改。
三大特性
可见性
JMM三部分可以展现出的问题是线程A读取内存中num1的值存放在工作内存中,此时进行了更改但是未写回主内存,现在线程B读取内存中的num1值是未被线程A修改前的数据,这是不正确的,所以必须保证变量的可见性,即某个线程修改对应变量值后立即写回主内存。Java代码中可以使用volatile或synchronized代码块保证。
有序性
如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本
身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对
其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。
原子性
由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个。基本数据类型访问和读写都满足原子性,除占用64位的long和double。Java语言提供了synchronized来保证原子性。
先行发生原则
如果用户在编码过程中时刻想着满足三大特性会使编码过程很累,JMM向我们屏蔽了底层细节,我们只需要满足先行发生原则即可。
先行发生是Java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到。
例如
int i = 1;
int j = i;
程序执行后J的值一定为1。
下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已
经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出
来,则它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。
- 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
- 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后。
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检
测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。 - 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程
的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。 - 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的
finalize()方法的开始。 - 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出
操作A先行发生于操作C的结论。
synchronized锁种类
自从JDK1.6之后,对于synchronized进行了优化,之前使用synchronized是重量级锁,现在将锁分为了4类。锁只会升级不会降级。每个对象中都会保存对象头称为Mark Word。
-
无锁
无线程获取时。 -
偏向锁
大部分情况下,锁不仅仅不存在多线程竞争, 而是总是由同一个线程多次获得,为了让线程获取锁的代价更低就引入了偏向锁的概念。-
偏向锁获取
如果当前对象Mark Word中的偏向模式为1,则代表可偏向,第一个进入该对象的线程会使用CAS 操作,将自己线程ID放置到Mark Word中,如果放置成功代表无其他线程同时获取该对象的偏向锁,如果放置失败则代表有其他线程同时竞争,则锁升级为轻量级锁。
如果当前对象Mark Word中的偏向模式为1,则代表已偏向,需要对比线程ID是否一致,若一致则直接进入代码块,若不一致升级为轻量级锁。 -
偏向锁的撤销
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或偏向其他线程或轻量级锁(标志位为00)的状态。
-
-
轻量级锁
当偏向锁执行时被另一个线程请求偏向,会将锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。
如果只有一个线程等待则会使用自旋锁,如果在自旋结束还未获取到锁,则锁膨胀为重量级锁。
如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
或者该对象调用过hashcode,将原本可偏向的对象变得不可偏向,此时将直接进入轻量级锁。- 重量级锁
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
深入理解Java虚拟机(周志明)阅读笔记,理解有限,如有错误还望指出。