要明白锁的原理,首先要知道对象头
Java对象头
在Java中,一个对象一般由两部分组成 :1、对象头 ; 2、对象的成员变量信息
在32位的虚拟机中:
(1)普通对象的对象头长度64bit(8字节):
其中的32bit是Mark Word,另外32位是Klass Word,如下:
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------|-------------------------------------|
(2)数组对象的对象头长度96bit(12自己):
除了Mark Word和Klass Word,还有32bit的 array length,如下:
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
什么是 Mark Word 什么是 Klass Word 什么是array length ?
- array length - 很明显这是标明数组长度的字段
- Klass Word - 对象类型指针,通过该字段标明当前对象所属的类
- Mark Word - 下面着重介绍
在不同状态下,Mark Word 会有不同的表现形式,对应关系如下:
Normal 就是对象的正常状态,以该状态为例
- 25位是对象的hashcode
- 4位是age,在垃圾回收中的分代年龄
- 1位biased_lock,标明是否是对象锁
- 2位(01),表明当前的加锁状态
Biased 是偏向锁状态
Lightweight Locked 是轻量级锁状态
Heavyweight Locked 是重量级锁状态
Marked for GC 是在JVM进行GC时的状态
无论是偏向锁、轻量级锁、重量级锁,都是在对象头中的Mark Word做文章
锁升级
在默认情况下,synchronized 会首先使用偏向锁,当发现存在竞争时,就会升级为轻量级锁,当竞争比较激烈时,轻量级锁就会升级为重量级锁
但是我们可以通过添加 VM 参数 -XX:-UseBiasedLocking
手动关闭偏向锁,那么synchronized 会直接进入轻量级锁
为什么会有锁升级?
因为从偏向锁 - 轻量级锁 - 重量级锁,互斥程度会越来越高,但是性能代价也越来越大,所以在一开始,如果资源竞争不是很激烈,应该优先选择程度较轻的锁,以下是《Java并发编程的艺术》一书中的原话
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状
态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏
向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高
获得锁和释放锁的效率,下文会详细分析。
重量级锁原理
操作系统会提供 Monitor 对象,对某个对象加上重量级锁之后,该对象头中的 Mark Word 中就被设置成指向 Monitor 对象的指针 即上图所示的 ptr_to_heavyweight_monitor:30
在Monitor对象中会有两个属性: 1、Owner ;2、EntryList
Owner - 指当前这个对象的锁是由哪个对象持有的
EntryList - 是一个线程队列,当其他线程来竞争该对象锁失败时,就会进入EntryList 中阻塞
因为重量级锁的加锁过程需要使用到操作系统提供的 Monitor 对象,换言之,属于内核级别,因此性能代价是较大的,如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
重量级锁的自选优化
重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),当前线程就可以避免阻塞
轻量级锁
每个线程的每个栈帧都会包含一个锁记录的结构
锁记录包含两个部分,其中一个部分用存储上锁对象的 Mark Word,另外一部分是对象指针 Object reference,用于指向上锁对象的地址
上锁过程如下,让锁记录中 Object reference 指向上锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,结果如下
如果cas 失败,有两种情况:
- 其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数,如下:
解锁时有如下两种情况
- 锁记录的值为 null,表示有重入,这时重置锁记录,表示重入计数减一
- 锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头,如果失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀过程
假设此时Thread-0已经对某个对象上了轻量级锁,现实Thread-1也尝试对改对象加锁,必然加锁失败,进入锁升级
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList 阻塞
如下图
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 Entry List 中 的Thread-1 线程
轻量级锁缺点
在很多情况下,一个对象在某段时间内,会不停被同一个线程上锁,即不断发生锁重入,这就会频繁的进行Mark Word的cas操作,这也是存在一定性能损耗的,因此诞生了偏向锁来优化
偏向锁
只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
比较Normal状态和偏向锁状态的Mark Word:
我们发现Normal状态下 biased_lock 是0,偏向锁的 biased_lock 是1; biased_lock 是偏向锁的开启标志,0表示禁用,1表示开启
而且对于Normal,用了 31bit 去存储对象的hashcode;
而偏向锁状态下不在记录对象的hashcode,而是记录线程编号:初始状态下 54bit 的thread标记都是0,当有线程对该对象上轻量级锁后, 54bit 的thread标记记录下当前线程编号,而且那么线程出了临界区释放了锁,thread不会发生改变,所以当该线程对该对象进行锁重入或者重新上锁时,发现里面的线程编号就是自己,就表示没有竞争,不用 CAS
而如果有另外一个线程要对该对象上锁时,发现当前记录着其他线程的编号,就进行锁膨胀,升级为轻量级锁