重量级锁
重量级锁其实就是 synchronized 最终加锁的过程,在 JDK 1.6 之前,就是由无锁 -> 加锁的这个过程。
重量级锁的获取流程
- 接着上面偏向锁的获取过程,由偏向锁升级为轻量级锁,执行下一步
- 会在原持有偏向锁的线程的栈中分配锁记录,将对象头中的 Mark Word 拷贝到原持有偏向锁线程的记录中,原持有偏向锁的线程获得轻量级锁,然后唤醒原持有偏向锁的线程,从安全点处继续执行,执行完毕后,执行下一步,当前线程执行第 4 步
- 执行完毕后,开始轻量级解锁操作,解锁需要判断两个条件
- 判断对象头中的 Mark Word 中锁记录指针是否指向当前栈中记录的指针
- 拷贝在当前线程锁记录的 Mark Word 信息是否与对象头中的 Mark Word 一致。
如果上面两个判断条件都符合的话,就进行锁释放,如果其中一个条件不符合,就会释放锁,并唤起等待的线程,进行新一轮的锁竞争。
- 在当前线程的栈中分配锁记录,拷贝对象头中的 MarkWord 到当前线程的锁记录中,执行 CAS 加锁操作,会把对象头 Mark Word 中锁记录指针指向当前线程锁记录,如果成功,获取轻量级锁,执行同步代码,然后执行第3步,如果不成功,执行下一步
- 当前线程没有使用 CAS 成功获取锁,就会自旋一会儿,再次尝试获取,如果在多次自旋到达上限后还没有获取到锁,那么轻量级锁就会升级为
重量级锁
如果用流程图表示是这样的
根据上面对于锁升级细致的描述,我们可以总结一下不同锁的适用范围和场景。
synchronized 代码块的底层实现
为了便于方便研究,我们把 synchronized 修饰代码块的示例简单化,如下代码所示
public class SynchronizedTest { private int i; public void syncTask(){ synchronized (this){ i++; } } }
我们主要关注一下 synchronized 的字节码,如下所示
从这段字节码中我们可以知道,同步语句块使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令指向同步代码块的结束位置。
那么为什么会有两个 monitorexit 呢?
不知道你注意到下面的异常表了吗?如果你不知道什么是异常表,那么我建议你读一下这篇文章
看完这篇Exception 和 Error,和面试官扯皮就没问题了
synchronized 修饰方法的底层原理
方法的同步是隐式的,也就是说 synchronized 修饰方法的底层无需使用字节码来控制,真的是这样吗?我们来反编译一波看看结果
public class SynchronizedTest { private int i; public synchronized void syncTask(){ i++; } }
这次我们使用 javap -verbose 来输出详细的结果
从字节码上可以看出,synchronized 修饰的方法并没有使用 monitorenter 和 monitorexit 指令,取得代之是ACC_SYNCHRONIZED 标识,该标识指明了此方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这就是 synchronized 锁在同步代码块上和同步方法上的实现差别。