前面我们说了 synchronized 底层由monitor实现的,它那 synchronized 到底锁的是什么呢?随着 JDK 版本的升级,synchronized 又做出了哪些改变呢?“synchronized 性能很差”的谣言真的存在吗?
在介绍以上内容之前,我们要先知道重量级锁概念。
重量级锁
当另外一个线程执行到同步块的时候,由于它没有对应 monitor 的所有权,就会被阻塞,此时控制权只能交给操作系统,也就会从 user mode 切换到 kernel mode, 由操作系统来负责线程间的调度和线程的状态变更, 这就需要频繁的在这两个模式下切换(上下文转换)。有点竞争就找内核的行为很不好,会引起很大的开销,所以大家都叫它重量级锁,自然效率也很低,这也就给很多小伙伴留下了一个印象 —— synchronized 关键字相比于其他同步机制性能不好,但其实不然。
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
一、MarkWord
在JVM虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充。
图片
我们需要重点分析MarkWord对象头,因为Markword 是保存锁状态的关键,对象锁状态可以从偏向锁升级到轻量级锁,再升级到重量级锁,加上初始的无锁状态,可以理解为有 4 种状态。想在一个对象中表示这么多信息自然就要用位来存储。
图片
- hashcode:25位的对象标识Hash码
- age:对象分代年龄占4位
- biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
thread:持有偏向锁的线程ID,占23位
- epoch:偏向时间戳,占2位
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
我们可以通过lock的标识,来判断是哪一种锁的等级
- 后三位是001表示无锁
- 后三位是101表示偏向锁
- 后两位是00表示轻量级锁
- 后两位是10表示重量级锁
二、轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
如果 CPU 通过 CAS(后面会细讲,戳链接直达)就能处理好加锁/释放锁,这样就不会有上下文的切换。
但是当竞争很激烈,CAS 尝试再多也是浪费 CPU,权衡一下,不如升级成重量级锁,阻塞线程排队竞争,也就有了轻量级锁升级成重量级锁的过程。
图片
作为程序员的我们最喜欢用代码说话,贴心的 openjdk 官网提供了可以查看对象内存布局的工具 JOL (java object layout),我们直接通过 Maven 引入到项目中。
org.openjdk.jol
jol-core
0.14
public class SyncSample {
private static Object LOCK = new Object();
public static void main(String[] args) {
System.out.println("----------未进入同步块,MarkWord 为:----------");
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
synchronized (LOCK) {
System.out.println("----------进入同步块,MarkWord 为:----------");
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable());
}
}
}
图片
2.1 加锁流程
在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
图片
通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
图片
如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
图片
如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
2.2 解锁流程
遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
图片
如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
图片
三、偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
图片
可是多线程环境,也不可能只有同一个线程一直获取这个锁,其他线程也是要干活的,如果出现多个线程竞争的情况,就会有偏向锁升级的过程。
在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
图片
通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。
图片
如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些。
图片
思考:偏向锁可以绕过轻量级锁,直接升级到重量级锁吗?
四、面试题
面试官:Monitor实现的锁属于重量级锁,你了解过锁升级吗?
Java中的synchronized有无锁(无锁就是没有对资源进行锁定,任何线程都可以尝试去修改它)、偏向锁、轻量级锁、重量级锁四种形式,偏向锁、轻量级锁、重量级锁分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况
锁别 | 描述 |
重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性 |
偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令 |