记录一下Java并发编程的知识点。有部分内容是借鉴《Java并发编程的艺术》这本书的。本次介绍一下Java并发机制的底层实现,主要是volatile和synchronized关键字。
volatile的应用
volatile可以保证并发过程中变量的“可见性”。同时可以禁止指令重排
“可见性”的原理
在了解volatile实现原理之前,先看看与其实现原理相关的CPU术语与说明。下表来自《Java并发编程的艺术》
还有就是为了提高处理速度,CPU处理器不会直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但是操作完不知道何时会写到内存。如下图
当有volatile变量修饰的共享变量进行写操作的时候,JVM会向处理器发送一条Lock前缀的指令,这个指令在多核处理器下会引发两件事:
这里第二点涉及到MESI(缓存一致性协议):每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。
禁止指令重排的原理
首先我们先看一下内存屏障的指令类型,如下表
为了实现volatile禁止指令重排,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。策略如下:
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图
下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图
synchronized的使用
利用synchronized实现同步的基础:Java中的每个对象都可以作为锁。具体表现为以下3种形式:
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或者抛出异常时也必须释放锁。那么锁到底存在哪里呢?
Java对象头
Java对象头主要由两部分组成:Mark Word(存储对象的hashCode或锁信息)和Class Metadata Address(存储到对象类型数据的指针),如果是数组对象,还有一个Array length来存储数组的长度。
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,存储结构可能存在下面5种变化。下图来自黑马程序员的课程。
synchronized锁的初步了解
从JVM规范中可以看到synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。细节实现是使用monitorenter和monitorexit指令实现的。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。
任何对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的Monitor的所有权,即尝试获得对象的锁。
而对象和Monitor关联就是通过对象头中的Mark Word。
Monitor
结合对象头和上面synchronized锁的初步了解,我们可以理解为当我们使用synchronized给对象加锁(重量级)后,该锁对象的对象头Mark Word中就被设置为指向Monitor对象的指针。
synchronized锁升级
jdk1.6以后,为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
偏向锁
1、偏向锁加锁
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头的Mark Word和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
2、偏向锁的撤销
轻量级锁
1、轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为DisplacedMark Word。
然后线程尝试使用CAS(比较并交换)将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。过程如下图
如果线程自己执行了synchronized锁重入,那么会再添加一条锁记录作为重入的计数。
2、轻量级锁解锁
轻量级解锁时,如果有锁记录为null的,表示有重入重置锁记录,表示重入计数减一。如果锁记录不为null,会使用原子的CAS操作将Displaced Mark Word替换回到对象头
3、锁膨胀过程
以上就是本次文章的内容了,感觉涉及到很多平时没有接触到的知识点,比如内存屏障、Monitor对象等等,一开始理解可能比较难,等接受了有这些东西的存在以后,就感觉一切都是那么顺其自然了。