前言
在很久之前,我在面实习生的时候,就有人问过我synchronized的锁升级过程,我当时只是浅浅了解,后面其实了解了锁升级的流程。但其实我并不是很明白,究竟优化了哪里,究竟是针对哪种场景进行优化,我其实更想得到这个锁升级过程中的引入场景。尤其是看到JDK 15废弃并禁用了偏向锁之后,我其实在想为什么要移除这项技术,是JDK 有了更好的优化,还是这项技术不再适用于现在。这里直接说答案吧,答案就在JEP 374中。我本来想直接贴答案的,但是考虑到有的同学还不清楚synchronized的升级流程,这里还是先简单的讲一下锁升级的流程。
其实这也是一道面试常见的问题,但是常常是面试官问我锁升级的过程,而不会问哪些场景会从锁升级中受益,这也是我常常疑惑的地方,不去问why,而是问what。
总有些口口流传的优化,大家都愿意相信,但是我们都要相信那句话: 没有调查就没有发言权。
synchronized锁简介
这里我们简单的复习一下synchronized, synchronized是我们遇到的第一个同步工具,它有许多别名: 内部锁、排他锁、悲观锁。它能够保障原子性、可见性和有序性。synchronized 关键字修饰的方法就被称为同步方法(Synchronized Method), synchronized修饰的静态实例方法就被称为同步实例方法。同步方法的整个方法称为临界区。
// 修饰方法 public synchronized void synchronizedDemo(){ } // 修饰静态方法 public static synchronized void synchronizedStaticDemo(){ } public void synchronizedDemoPlus(){ // 修饰代码块 synchronized (this){ } }
Java平台的任何一个对象都有唯一一个与之关联的锁。 线程进入临界区需要申请锁,那么锁放在哪里呢? 答案是对象头,一个普通的Java对象的内部构造如下图所示:
一般来说,我们对这个锁的认知是,多个线程进入临界区的时候,会申请获得这个锁,如果锁已经被其他线程所获取,那么这个线程会陷入阻塞状态。
更为准确的描述是JVM会为每个内部锁分配一个入口集,用于记录等待获得相应内部锁的线程,当这些线程申请的锁被其持有线程释放的时候,该锁入口集中的一个任意线程会被JVM唤醒。看到这里可能有同学会问,上面提到了获得锁和释放锁,JVM是怎么处理的呢。 这个其实要借助反编译指令,这里我们以同步代码块来观察synchronized的内部实现:
/** * 这个代码要编译一下,形成字节码, * 其实问题的答案就在字节码上。 */ public class ThreadDemo{ public void synchronizedDemo(){ synchronized (this){ } } }
然后找到这个类对应的字节码所在的文件夹,打开命令行执行如下指令
javap -c ./ThreadDemo.class
monitorenter 代表进入临界区和申请锁指令,monitorexit代表出临界区,释放锁指令。那为什么会有两个释放锁指令,这个问题问的好,最下面的那个释放锁指令是为临界区的代码出了异