20.synchronized原理详解

青旅半醒 2024-02-05 18:51 115阅读 0赞

在java中,最简单的同步方式就是利用 synchronized 关键字来修饰代码块或者修饰一个方法,那么这部分被保护的代码,在同一时刻就最多只有一个线程可以运行,而 synchronized 的背后正是利用 monitor 锁实现的。

那么?

获取和释放 monitor 锁的时机是什么呢?

获取和释放 monitor 锁的时机,每个 Java 对象都可以用作一个实现同步的锁,这个锁也被称为内置锁或 monitor 锁,获得 monitor 锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法,线程在进入被 synchronized 保护的代码块之前,会自动获取锁,并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。

例子:

  1. public synchronized void method() {
  2. method body
  3. }

method() 方法是被 synchronized 修饰的,上面这段代码改写为伪代码。

  1. public void method() {
  2. this.intrinsicLock.lock();
  3. try{
  4. method body
  5. }
  6. finally {
  7. this.intrinsicLock.unlock();
  8. }
  9. }

在这种写法中,进入 method 方法后,立刻添加内置锁,并且用 try 代码块把方法保护起来,最后用 finally 释放这把锁,这里的 intrinsicLock 美就是 monitor 锁。

用 javap 命令查看反汇编的结果

JVM 实现 synchronized 方法和 synchronized 代码块的细节是不一样的。

同步代码块

  1. public class SynTest {
  2. public void synBlock() {
  3. synchronized (this) {
  4. System.out.println("text");
  5. }
  6. }
  7. }

在 SynTest 类中的 synBlock 方法,包含一个同步代码块,synchronized 代码块中有一行代码打印了 text 字符串。

下面我们来通过命令看下 synchronized 关键字到底做了什么事情:

  • 首先用 cd 命令切换到 SynTest.java 类所在的路径
  • 然后执行 javac SynTest.java,于是就会产生一个名为 SynTest.class 的字节码文件
  • 然后我们执行 javap -verbose SynTest.class,就可以看到对应的反汇编内容。

大概下面这样:

  1. public void synBlock();
  2. descriptor: ()V
  3. flags: ACC_PUBLIC
  4. Code:
  5. stack=2, locals=3, args_size=1
  6. 0: aload_0
  7. 1: dup
  8. 2: astore_1
  9. 3: monitorenter
  10. 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  11. 7: ldc #3 // String text
  12. 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  13. 12: aload_1
  14. 13: monitorexit
  15. 14: goto 22
  16. 17: astore_2
  17. 18: aload_1
  18. 19: monitorexit
  19. 20: aload_2
  20. 21: athrow
  21. 22: return

从里面可以看出,synchronized 代码块实际上多了 monitorenter 和 monitorexit 指令,标红的第3、13、19行指令分别对应的是 monitorenter 和 monitorexit。

这里有一个 monitorenter,却有两个 monitorexit 指令的原因是,JVM 要保证每个 monitorenter 必须有与之对应的 monitorexit,monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处和异常处两个地方,这样就可以保证抛异常的情况下也能释放锁

可以把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,我们来具体看一下 monitorenter 和 monitorexit 的含义:

monitorenter
执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:

  • 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。
  • 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。
  • 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。

monitorexit

monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。

同步方法

而对于 synchronized 方法,并不是依靠 monitorenter 和 monitorexit 指令实现的,被 javap 反汇编后可以看到,synchronized 方法和普通方法大部分是一样的,不同在于,这个方法会有一个叫作 ACC_SYNCHRONIZED 的 flag 修饰符,来表明它是同步方法。

  1. public synchronized void synMethod() {
  2. }

对应的反汇编指令如下所示:

  1. public synchronized void synMethod();
  2. descriptor: ()V
  3. flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  4. Code:
  5. stack=0, locals=1, args_size=1
  6. 0: return
  7. LineNumberTable:
  8. line 16: 0

可以看出,被 synchronized 修饰的方法会有一个 ACC_SYNCHRONIZED 标志。

当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。

发表评论

表情:
评论列表 (有 0 条评论,115人围观)

还没有评论,来说两句吧...

相关阅读

    相关 synchronized原理

    ![4e33ae1c68c51a0a3dbc2deb7e141702.png][] 一.锁升级 synchronized相对比较‘智能’,会根据锁竞争的激烈程度进行锁升级,

    相关 20.synchronized原理详解

    在java中,最简单的同步方式就是利用 synchronized 关键字来修饰代码块或者修饰一个方法,那么这部分被保护的代码,在同一时刻就最多只有一个线程可以运行,而 sync

    相关 Synchronize 原理

    1. 线程的状态 Thread.State 枚举,将线程划分为六中状态。 ![image-20210531180611460][] `new` 线程刚被创建,但是还没