字节码分析synchronized原理

同步控制指令

Java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的

方法级的同步

方法级的同步:是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法;

当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置。

• 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁。
• 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。
• 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

举例:

private int i = 0;
public synchronized void add() {
    i++;
}

对应的字节码:

0  aload_0
1  dup
2  getfield #2 <com/atguigu/java1/SynchronizedTest.i>
5  iconst_1 
6  iadd
7  putfield #2 <com/atguigu/java1/SynchronizedTest.i>
10 return

这段代码和普通的无同步操作的代码没有什么不同,没有使用monitorenter和monitorexit进行同步区控制。

这是因为,对于同步方法而言,当虚拟机通过方法的访问标示符判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁。

因此,对于同步方法而言,monitorenter和monitorexit指令是隐式存在的,并未直接出现在字节码中。

方法内指令指令序列的同步

同步一段指令集序列:通常是由java中的synchronized语句块来表示的。jvm的指令集有monitorenter和monitorexit 两条指令来支持synchronized关键字的语义。

当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。

当线程退出同步块时,需要使用monitorexit声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。

指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的。

下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。

对应的字节码:

 0: aloade
 1: dup
 2: astore_1
 3: monitorenter
 4: aload_0
 5: dup
 6: getfield #2 //Field i:I
 9: iconst_1
10: isub
11: putfield #2 //Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
26: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
    from to target type
    4    16    19  any
    19   22    19  any

编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。

为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令

# Java 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×