前言
在 Java并发编程(四)——synchronized关键字实现加锁 中我们学习了在并发场景下最简单的同步方式就是利用 synchronized 关键字来修饰代码块或者修饰一个方法,那么这部分被保护的代码,在同一时刻就最多只有一个线程可以运行,而 synchronized 的背后正是利用 monitor 锁实现的。
所以首先我们来看下获取和释放 monitor 锁的时机,每个 Java 对象都可以用作一个实现同步的锁,这个锁也被称为内置锁或 monitor 锁,获得 monitor 锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法,线程在进入被 synchronized 保护的代码块之前,会自动获取锁,并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。
代码示例:
1 | public synchronized void method() { |
为了方便理解其背后的原理,我们把上面这段代码改写为下面这种等价形式的伪代码:
1 | public void method() { |
在这种写法中,进入 method 方法后,立刻添加内置锁,并且用 try 代码块把方法保护起来,最后用 finally 释放这把锁,这里的 intrinsicLock 就是 monitor 锁。
JVM 实现 synchronized 方法和 synchronized 代码块的细节是不一样的,下面我们分别来看一下两者的实现。
synchronized同步代码块
编写一个Java文件,命名为SynTest:
1 | public class SynTest { |
然后在Windows下cd命令进入该文件所在目录,使用javac进行编译成class文件
1 | javac SynTest.java |
然后使用javap命令可以看到对应的反汇编内容:
1 | javap -verbose SynTest.class |
我们需要关注下monitorenter指令和monitorexit指令,可以把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁。可以看到一个monitorenter对应两个monitorexit指令,
这是因为monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处和异常处两个地方,这样就可以保证抛异常的情况下也能释放锁
执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。
monitorenter
执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:
-
如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。
-
如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。
-
如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。
monitorexit
monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。
synchronized同步方法
同样的编写一个SynTest类,里面编写一个synBlock方法,该方法加上synchronized关键字:
1 | public class SynTest { |
经过编译及反汇编得到指令如下:
可以看出,被 synchronized 修饰的方法会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。其他方面, synchronized 方法和刚才的 synchronized 代码块是很类似的,例如这时如果其他线程来请求执行方法,也会因为无法获得 monitor 锁而被阻塞。
总结
我们深入了解了synchronized关键字的底层原理,synchronized同步代码块和方法的底层实现有些许的区别,同步代码块是通过monitorenter和monitorexit指令,同步方法是通过叫ACC_SYNCHRONIZED的flag修饰符来实现。其他方面比如维护被锁次数的计数器原理都是类似的。