一、简介
java在1.6之后为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁的概念,在jdk 1.6中,锁分为四种状态,从低到高为:无锁,偏向锁,轻量级锁,重量级锁。这几个状态会随着线程竞争不断升级,但是升级后的状态不会再降级。
二、对象头
在介绍锁的状态升级之前,我们先了解一下,java对象头的基本概念。
java对象头分为三个部分:
- Mark Word
- 对象数据的指针
- 数组对象的长度(如果是数组的话)
synchronized用的锁是存在Java对象头的Mark Word里。我们这里主要看看Mark Word里面不同锁状态下存储的是什么。
从上面的图中,有一个分代年龄,GC回收的年龄为什么默认是15 ,因为在Mark Word中分代年龄只有4bit,而4bit最大也只能是15。
三、锁的状态
无锁:是最理想的状态,线程无须任何竞争便可以直接访问同步代码块的内容。
偏向锁:偏向于第一个访问的线程。
- 线程A访问同步快,判断对象头Mark Word 里的锁标识是否为偏向锁。是走2,不是走3。
- 检查Mark Word里的线程id是不是指向当前线程,如果是走4,不是走3。
- CAS操作替换Mark Word线程id指向当前线程,成功走4,不成功走5.
- 获得偏向锁,执行同步代码块的内容。
- 开始执行偏向锁的撤销,等待原持有偏向锁的线程到达安全点,暂停当前线程,检查原持有偏向锁线程的线程状态。线程状态不是活动或 者已经退出同步代码块走6,否则走7。
- 原持有线程开始释放锁,修改Mark Word 里的线程id为空,唤醒暂停线程,继续按1的步骤开始走下去。
- 锁膨胀,开始升级为轻量级锁。
轻量级锁:本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
- 线程访问同步块,检查锁的状态是否01,是走上面的偏向锁流程,不是走第二步。
- 线程栈中分配Lock Record空间,复制对象头的Mark Word 到线程锁记录里,官方名词 Displaced Mark Word 。
- CAS替换对象头Mark Word的锁记录指针指向到线程栈中新分配的锁记录,替换成功走5,不成功走4。
- 自旋,循环执行3,当自旋达到一定次数时,仍然没有获取成功,锁开始升级成为重量级锁,当前线程挂起。
- 线程获取轻量级锁成功,开始执行同步代码块内容。
- 同步代码块内容执行完毕,开始释放轻量级锁。
- CAS操作,将线程栈中的 Displaced Mark Word 替换回对象头的Mark Word,替换成功则直接释放锁。替换失败则释放锁并且唤醒暂停的线程。
相比较偏向锁,轻量级锁多了一个 Displaced Mark Word 的动作,在线程栈中复制原对象的Mark Word ,在使用完之后在替换回去。
为什么轻量级锁在释放锁的时候,在CAS操作替换失败之后,要唤醒暂停的线程?
CAS操作失败,说明对象头的Mark Word 已经被改变,当前对象可能有多个线程在竞争,只有在替换锁标识的时候,对象头的Mark Word才可能被改变,这时已经是轻量级锁,不可能降级,所以锁已经升级成重量级锁,其他竞争的线程已经被挂起。释放锁的线程需要唤醒其他被挂起的线程,让他们继续竞争。
重量级锁: 理论上,无锁,偏向锁,和轻量级锁都是乐观锁,设计的本意是倾向于无竞争,或者竞争很小的情况下使用,用来减少重量级锁使用操作系统频繁切换内核态的性能损耗,当锁的竞争加剧的时候,乐观锁并不能带来足够的性能提升,相反可能回导致资源消耗加大,这时,锁的状态就会直接升级成重量级锁。
结合偏向锁和轻量级锁的升级和撤销过程,下图是锁在几个状态如何撤销升级的流程。
线程在访问同步块的时候,判断锁的状态:
- 锁标识是01的时候,直接走偏向锁的链路。
- 锁标识是00 的时候,直接走轻量级锁的链路。
- 锁标识是10的时候,直接走重量级锁的链路,挂起线程。
关于轻量级锁如何膨胀到重量级锁:
- 自旋到一定次数(使用-XX:+UseSpinning参数来开启,在JDK 1.6中就已经改为默认开启了。自旋次数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。),
- 在自旋时,有第三个线程来竞争。