多线程与高并发(一)—— 自顶向下理解Synchronized实现原理


一、 什么是锁?

在多线程中,多个线程同时对某一个资源进行访问,容易出现数据不一致问题,为保证并发安全,通常会采取线程互斥的手段对线程进行访问限制,这个互斥的手段就可以称为锁。锁的本质是状态+指针,当一个线程进入临界区前需要先修改状态,表明已加锁,并且指针指向加锁的线程。后续线程在进入临界区时同样需要尝试修改状态,修改状态前首先检查指针是否为空,如果不为空且指向其他线程则表明已经有其他线程占用了锁,则无法进行状态修改,也就是此线程获取锁失败。

二、 Synchronized 锁原理

Synchronized 关键字如何实现同步互斥?

一、生成字节码

首先了解 Synchronized 的三种用法:

  • 锁对象实例
  • 锁方法实例
    以上三种不同的使用方式,JVM 生成的字节码也不同,具体如下:
  1. 锁对象实例
Synchronized(this) {
}

通过 反编译生成的字节码可看到,生成了字节码指令 monitorenter 和 monitorexit;当代码执行到monitorenter时加锁,执行monitorexit时解锁。Exception table 意为异常跳表, 如下,该异常表监测了7-13行的指令,也就是同步块,如果在同步块中出现了异常导致无法解锁,指令会跳转到 target 16 行执行,如此便能保证即使出现异常也不会导致永远无法退出锁。

public void test();
    Code:
       0: aload_0
       1: getfield      #3                  // private Object lock = new Object();
       4: dup
       5: astore_1
       
       6: monitorenter
       
       7: aload_0
       8: invokevirtual #4                  // Method foo:()V
       
      11: aload_1
      12: monitorexit
      13: goto          21
      
      16: astore_2
      17: aload_1
      18: monitorexit
      19: aload_2
      20: athrow
      21: return
    Exception table:
       from    to  target type
           7    13    16   any
          16    19    16   any

  1. 锁方法实例
synchronized public void test() {
}

方法级别的同步不会生成 monitorenter 和 monitorexit 指令,通过常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现,JVM在调用方法时,对方法的符号引用(flags)进行解析,ACC_PUBLIC 为公共方法,ACC_SYNCHRONIZED 为同步方法,如果此方法是同步方法则会进行加锁。

public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED

二、字节码如何执行?

  1. JVM 初始化时会为每个字节码指令都创建一个模板,每个模板都关联到其对应的汇编代码生成函数。以 HotSpot jdk8 为例,该模板位于src/share/vm/interpreter/templateTable.cpp(源码地址:https://cseweb.ucsd.edu/classes/sp16/cse120-a/applications/ln/lecture9.html
    [3] [oracle 官方文档]:https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html