【线程、锁】通过javap工具查看字节码观察synchronized底层实现

前言

synchronized可以加载锁,那么是底层是如何实现的呢?

synchronized根据语法,作用在不同地方,底层实现不同。

1. synchronized锁代码块

代码,synchronized锁代码块:

public class Juc_LockOnThis {

    public void decrStock(){
        //synchronized锁代码块
        synchronized (this){
                System.out.println("hello world");
        }
    }
}

通过javap -v 查看字节码:

  public void decrStock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: `monitorenter`
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String hello world
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: `monitorexit`
        14: goto          22
        17: astore_2
        18: aload_1
        19: `monitorexit`
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any

从字节码中可知同步语句块的实现使用的是monitorentermonitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取mark word里面存储的monitor,当 monitor的进入计数器为 0,那线程可以成功取得monitor,并将计数器值设置为1,取锁成功。

如果当前线程已经拥有 monitor 的持有权,那它可以重入这个 monitor ,重入时计数器的值也会加 1。倘若其他线程已经拥有monitor的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor并设置计数器值为0 ,其他线程将有机会持有 monitor 。

值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从上面的字节码中也可以看出有两个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

可以简单理解为默认在代码中额外加了一个try {} finally{ exit} 语句,确保一定能执行到解锁的代码

2. Synchronized锁整个方法

同步方法的加锁、解锁是通过 Javac 编译器实现的,底层是借助ACC_SYNCHRONIZED访问标识符来实现的,代码如下所示:

public class Hello {
    public synchronized void test() {
        System.out.println("test");
    }
}

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。

JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有monitor,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

下面我们看看字节码层面如何实现:

  public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, `ACC_SYNCHRONIZED`    '重点再这里,方法层面的标志'
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/yg/edu/Test;

flags 是多位标志,ACC_PUBLIC表示方法是public的,而ACC_SYNCHRONIZED表示方法是同步的。

参考:
《看完这篇恍然大悟,理解Java中的偏向锁,轻量级锁,重量级锁》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值