Java并发系列(二):从字节码认识synchronized

590 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

前景回顾

上一章节我们介绍了Java中锁的概念,一起认识了锁是什么,为什么要有锁。文章直达

今天我们来看看synchronized这个玩意到底是怎么起作用的。

编译与反编译

不知道大家有没有用过java命令去编译一个class文件,如果没有,那么接下来还不认真学习一下?

主程序

先写一个类,啥也没有,就一个普通的打印“Hello World!”方法。

梦开始的地方。

public class Sync {
    public void add() {
        System.out.println("Hello World!");
    }
}

好,写完了,干嘛呢?

编译一下咯。

有小伙伴会问,main方法都没有,能编译成功么?

当然能!main方法是程序执行入口,这个类没main方法说明不在这执行主程序罢了!

OK,我们尝试编译一下。

编译

使用javac Sync.java编译指令进行编译,生成了Sync.class文件。

反编译

怎么看Sync.class文件呢?

使用javap -c Sync.class编译指令进行反编译,输出文件信息。

看看里面是啥:

Compiled from "Sync.java"
public class com.maicim.study.Sync {
  public com.maicim.study.Sync();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void add();
    Code:
       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
}

可以看到里面包含了类信息,方法信息等等,不认识的可以自行百度。

是不是很简单,又学到一招,可以拿去给小白吹牛了。

synchronized

言归正传,我们想看看synchronized在字节码里是怎样的形式以及怎么起作用的。

修改主程序,方法内给对象加上synchronized关键字:

public class Sync {
    public void add() {
        synchronized (this) {
            System.out.println("Hello World!");
        }
    }
}

同样的执行编译和反编译,看看结果:

Compiled from "Sync.java"
public class com.maicim.study.Sync {
  public com.maicim.study.Sync();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void add();
    Code:
       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
}

是不是发现有点不同了?

我知道你们也看不出什么不同,算了我直接说吧。

monitorenter和monitorexit

在上面的结果中,我把重点部分圈出来让大家看到更加明显。

Snipaste_2022-07-29_00-57-39.png

可以看到,add方法内部出现了monitorentermonitorexit

没错,它就是synchronized关键字在class文件中的映射。

monitorentermonitorexit之间的指令代表锁区间。

指令一旦进入monitorenter,代表当前线程获取到了锁,其余也想获取资源的线程全部阻塞,自动排队等待抢占锁。

指令一旦退出monitorexit,代表当前线程释放了锁,其余也想获取资源的线程开始抢占锁。

它们一般是成对出现的,那这里为啥会有两个monitorexit

因为在程序在持有锁的过程中可能会出现异常,需要异常退出,并释放锁。

有一个这样的面试题:

持有锁的线程出现异常后,会释放锁么?

会啊,当让会,你觉得设计锁的大佬会没考虑到这一点吗?要是问为啥会,请你把我这篇文章拍到面试官的脸上。

那么是一定有两个monitorexit吗?

不一定。若是主动抛出异常,则只会有一个。

修改主程序为这样:

public class Sync {
    public void add() {
        synchronized (this) {
            System.out.println("Hello World!");
            throw new RuntimeException("error");
        }
    }
}

自己去看看反编译的结果是啥吧,我就不贴上来了。

只要明白这一点:

jvm必须保证锁的正常获取和释放。

总结

今天带大家学习了怎么使用命令进行编译java代码文件和反编译查看字节码,并对synchronized的底层实现有了初步了解。

认识了monitorentermonitorexit以及它们的作用。

简单阐述了一下线程获取锁的场景。

但是我们也仅仅是了解了浅层原理而已,对于synchronized这把锁,我接下来还要继续深挖,看看它到底是怎么一回事。

一起听歌吧

《路过人间》 -- 郁可唯

快快抹干眼泪 看昙花多美

路过人间 无非一瞬间

每段并肩 都不过是擦肩

曾经辜负哪位 这才被亏欠

路过人间 一直这轮回