其实在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁
),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
本文先介绍synchronized锁升级之无锁和偏向锁,后续详细介绍下synchronized锁升级过程。
无锁
为了优化synchronized锁的效率,在JDK6中,HotSpot虚拟机开发团队提出了锁升级的概念,包括偏向锁、轻量级锁、重量级锁等,锁升级指的就是“无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁
”。
synchronized同步锁相关信息保存到锁对象的对象头里面的Mark Word中,锁升级功能主要是依赖Mark Word中锁标志位和是否偏向锁标志位来实现的。
从上图我们可以看到,无锁对应的锁标志位是“01”,是否偏向锁标志是“0”。
1 | public class NoLock { |
MarkWord对象头总共占8个字节,共64位,我们按照上图中“1 -> 8”,也就是从后面往前面拼接起来:
00000000 00000000 00000000 01110100 10100001 01000100 10000010 00000001● 红色字体:25位,不使用;
● 蓝色字体:31位,表示对象的hashCode,可以看到,跟程序输出结果对应的上;
● 橙色字体:1位,不使用;
● 青色字体:4位,对象的分代年龄;
● 紫色字体:1位,表示对象是否启用偏向锁标记。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁;本示例中,是无锁,所以是0;
● 黄色字体:2位,表示锁状态的标记位;本示例中,是无锁,所以是01;
偏向锁
什么是偏向锁?
HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
偏向锁的“偏”,它的意思是锁会偏向于第一个获得它的线程,会在对象头(Mark Word中)记录锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。
如下图是偏向锁对象头MarkWord布局:
1 | public static void main(String[] args) { |
如上我们看到,markword的倒数三位是000,根据前面的图,000表示的是轻量级锁,此时只有一个线程访问,为什么输出来的不是偏向锁标识101呢?
原因其实是偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟(默认延迟4秒)之后才会激活,可以使用 -XX:BiasedLockingStartupDelay=0
参数关闭延迟,让其在程序启动时立刻启动。当然为了演示,也可以在程序中休眠5秒,等待偏向锁激活后。
下面我们添加运行时JVM参数,再次启动程序,观察内存布局:
可以看到关闭偏向锁延迟后,当前锁就是偏向锁了。
偏向锁原理
在偏向锁第一次被线程拥有的时候,在偏向锁的MarkWord中,有一块区域用来记录偏向线程的ID。
注意,偏向锁只有遇到其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁的。
偏向锁的撤销
前面提到,大部分情况下,锁都是被同一个线程获取到,持有偏向锁的线程不会主动释放锁。
那么大部分情况下,不会涉及到偏向锁的撤销,当有另外的线程尝试竞争偏向锁的时候,这个时候才会涉及偏向锁的撤销流程。