通过jclasslib分析java字节码-synchronized原理
说明
原理:synchronized代码块同步,即一段指令系列的同步,主要是通过monitorenter、monitorexit来实现的,为了确保执行monitorenter后必须执行monitorexit,编译器会自动产生一个异常处理器,目的就是用来执行monitorexit;
下面将对下面代码进行字节码分析
package com.ydfind.test;
public class Monitor {
private void test() {
synchronized (this) {
int a = 1;
}
}
synchronized
public void test1() {
}
}
synchronized代码块分析
字节码
字节码及注释如下:
0 aload_0 // 将this压入操作数栈
1 dup // 将栈顶this复制并压入栈
2 astore_1 // 将栈顶this放入变量1。变量1是准备做为monitorexit的参数
3 monitorenter // 尝试持有栈顶this的管程(this出栈后,栈就空了)
// 下面2行实现int a = 1功能,1保存在变量2里面
4 iconst_1
5 istore_2
6 aload_1 // monitorexit this
7 monitorexit
8 goto 16 (+8)
11 astore_3 // 发生异常,将异常对象放入变量3----为什么不放入变量2呢?
12 aload_1 // monitorexit this
13 monitorexit
14 aload_3 // monitorexit后再抛出异常
15 athrow
16 return
杂项
上面字节码astore_n中n最大为3,及局部变量表深度4;操作数栈开始时两个this,最大深度为2。与杂项里面的记录一致:
异常表
起始PC:异常处理器 监控代码的起始位置
结束PC:异常处理器 监控代码的结束位置(并不包括结束行,即[startPC,endPC))
跳转PC:处理异常的起始位置,比如catch代码块
捕获类型:异常处理器所捕获的异常类型
关于异常表如下所示。若是同步代码块出现异常,会跳到11行,正常情况下,释放管程后,再抛出异常;但若11-13行发生异常,会不断跳到11行,即不断尝试释放管程,这是确保异常发生时能确保管程能释放的保护机制。
行号表
起始PC: 字节码的起始位置;
行号:代码行号;
操作数栈
起始位置:变量作用域的起始位置
长度:变量作用域的长度。例子中this在0-16总共17行中生效,即函数内都有效;
序号:数组的下标
名字:变量的描述(变量类型+引用类型 or 基本类型)
synchronized方法分析
方法级的同步是隐式的,在方法的调用和返回操作之中实现。方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED标志是否被设置了,若是被设置了,则当前线程需要持有管程才能执行方法,执行完成则会释放管程。在方法执行过程中,执行线程持有管程,故其它线程会无法获得管程而无法执行程序(
竞争失败的线程会被放入entrylist
);若是发生异常,且函数内部无法处理,那么管程会在 异常向外方法抛出时 自动释放。
上面可以看出,方法的访问标识里面有synchronized,则表示是同步方法。
运行时,jvm可以通过检查方法常量池的方法表结构的ACC_SYNCHRONIZED访问标志 得知该方法 是否被声明为同步方法。既然是方法常量池,则是在方法区。