面试过java岗位的同学,应该会很多次遇到过这个问题:synchronized这个关键字,它的原理在字节码是怎么体现的?今天我们来分析一下
首先,我们要明确,synchronized这个关键字用在方法上有2种使用方式:一是直接作为方法的修饰符,二是作为方法里synchronized块出现
synchronized直接作为方法修饰符
我们先看第一种情况——直接作为方法的修饰符
public synchronized void test() {
}
看看生成的字节码对应的助记符是怎样的
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/bill/MyByteCodeTest2;
看到这里,我们就会发现,synchronized修饰的方法其实跟普通方法没太大区别,只是在flags多了个ACC_SYNCHRONIZED,这样jvm在执行这个方法的时候,就会知道,这是个synchronized方法
synchronized作为方法里的一个方块出现
我们直接上代码
public void test2(Object o) {
synchronized (o) {
}
}
再看看生成的字节码对应的助记符是怎样的
public void test2(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: aload_2
5: monitorexit
6: goto 14
9: astore_3
10: aload_2
11: monitorexit
12: aload_3
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
LineNumberTable:
line 5: 0
line 6: 4
line 7: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this Lcom/bill/MyByteCodeTest2;
0 15 1 o Ljava/lang/Object;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class com/bill/MyByteCodeTest2, class java/lang/Object, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
大家把注意力放在Code里面的指令里,在这些指令里,我们重点关心2个指令——monitorenter和monitorexit
monitorenter
这个指令的作用是获取到操作数栈顶的引用,并且去获取该引用对应对象的Monitor锁。如果获取成功,则继续往下执行;如果获取失败,则线程阻塞,直到获取到这个锁。
这里简单讲一下对象的Monitor锁,java里每一个对象的对象头里都有一个monitor监视器,监视器里有几个属性——指向持有ObjectMonitor对象的线程(_owner)、锁的重入次数(_recursions)、用来记录该线程获取锁的次数(_count)。线程获取锁的步骤
(1)判断_owner是否为null,如果是,则获取锁成功,如果不是,转(2)
(2)判断_owner对应的线程对象是不是当前线程,如果是,也获取锁成功,视为重入,重入次数_recursions加1;如果不是,转(3)
(3)获取锁失败,该线程获取锁的次数(_count)加1
ps:操作数栈和局部变量表的概念前文有提及,如果不懂的话这里给个传送门 jvm之字节码解读——番外篇之经典面试题:e=(a+b-c)*d的执行过程
monitorexit
这个指令比较简单,作用是释放当前对象的锁
我们来看看获取锁对应的字节码指令解释
0: aload_1 // 将对象o的引用从局部变量表入操作数栈(局部变量表的_0=this,局部变量表的_1=对象0)
1: dup // 将栈顶的对象o的引用复制并再入栈
2: astore_2 // 将对象o的引用(新复制来的)pop出来加载入局部变量表,意味这是新元素(栈中的新o ——>成为 局部变量表的_2)
3: monitorenter // 锁住当前栈顶元素(即老对象的引用o,实际上对应的是同一个对象)的monitor
4: aload_2 // 将局部变量表中的新o入栈(局部变量表的_2=新h)
5: monitorexit
6: goto 14
9: astore_3
10: aload_2
11: monitorexit
12: aload_3
13: athrow
14: return
整个执行过程就是如同上面代码的解释一样执行了