简单分析synchronized不会锁泄漏的原因

最近看到一句话:内部锁synchronized不会造成锁泄漏(Lock Leak)。

锁泄漏是指一个线程获得某个锁以后,由于程序的错误、缺陷致使该锁一直没法被释放而导致其他线程一直无法获得该锁的现象。(摘自《Java多线程编程实战指南(核心篇)》--黄文海)

很好奇JVM是怎么保证的。

我想,Java代码,最终无非是编译成字节码,变成一条条指令,或许可以从指令入手研究一下。

1. 一个小例子

我们先来看看下面的代码。

 1 public class SynchronizedTest {
 2 
 3     private static final Object LOCK = new Object();
 4 
 5     public static void main(String[] args) {
 6         synchronized (LOCK) {
 7             foo();
 8         }
 9     }
10 
11     public static void foo() {
12         //do something...
13     }
14 }

很简单的一段代码,在临界区调用了一个方法。

通过javap -c SynchronizedTest.class ,得到如下信息,重点关注红框部分:

 简单分析synchronized不会锁泄漏的原因

第5和第10行分别是monitorenter 和 monitorexit,这表示进入和退出临界区,临界区中间第6行调用了foo()这个方法。当退出临界区后,第11行直接goto第19行,即return,从当前方法返回,方法结束。

这是正常情况下的执行顺序。

那么如果在临界区中发生了异常(本例中是foo()可能发生异常),执行顺序又是怎么样的呢?

2. 异常表

在研究执行顺序之前,先学习一下异常表。

异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。

上图的 Exception table即为异常表。它列举了哪些代码行的范围内,可能出现某种异常时,对应的处理机制。

那么异常表用在什么时候呢?

答案是异常发生的时候,当一个异常发生时

1.JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理

2.如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。

3.如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目

4.如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作。

5.如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止。

6.如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行。

以上就是JVM处理异常的一些机制。

】--以上摘自 详解JVM如何处理异常

具体到本例中

1. 当临界区发生异常,JVM查找异常表,发现符合第一行记录(异常发生点在6和11行(不包括第11行)之间,任意异常类型),于是跳到第14行,即第10行的monitorexit要么不执行,要么执行抛出异常,总之无法成功执行。

2. 从14行一路执行下去,到16行,再次monitorexit,此时释放锁。18行抛出上面获取到的异常,19行从当前方法返回void, 结束方法的运行。

3. 如果第2步过程中又发生异常,根据异常表第二行记录(异常点发生在14和17行(不包括第17行)之间,任意异常类型),会再次跳到14行,重新执行第2步,循环直到monitorexit指令成功执行。

这样就保证了monitorexit一定能够执行成功,锁一定会被释放。

上一篇:死磕 java同步系列之synchronized解析


下一篇:树莓派获取CPU温度