乐趣区

synchronized-原理知多少

synchronized是 Java 编程中的一个重要的关键字,也是多线程编程中不可或缺的一员。本文就对它的使用和锁的一些重要概念进行分析。

使用及原理

synchronized 是一个重量级锁,它主要实现同步操作,在 Java 对象锁中有三种使用方式:

  • 普通方法中使用,锁是当前实例对象。
  • 静态方法中使用,锁是当前类的对象。
  • 代码块中使用,锁是代码代码块中配置的对象。

使用

在代码中使用方法分别如下:

普通方法使用:

/**
 * 公众号:ytao
 * 博客:https://ytao.top
 */
public class SynchronizedMethodDemo{public synchronized void demo(){// ......}
}

静态方法使用:

/**
 * 公众号:ytao
 * 博客:https://ytao.top
 */
public class SynchronizedMethodDemo{public synchronized static void staticDemo(){// ......}
}

代码块中使用:

/**
 * 公众号:ytao
 * 博客:https://ytao.top
 */
public class SynchronizedDemo{public void demo(){synchronized (SynchronizedDemo.class){// ......}
    }
}

实现原理

方法和代码块的实现原理使用不同方式:

代码块

每个对象都拥有一个 monitor 对象,代码块的 {} 中会插入 monitorentermonitorexit指令。当执行 monitorenter 指令时,会进入 monitor 对象获取锁,当执行 monitorexit 命令时,会退出 monitor 对象释放锁。同一时刻,只能有一个线程进入在 monitorenter 中。

先将 SynchronizedDemo.java 使用 javac SynchronizedDemo.java 命令将其编译成 SynchronizedDemo.class。然后使用javap -c SynchronizedDemo.class 反编译字节码。

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

  public void demo();
    Code:
       0: ldc           #2                  // class SynchronizedDemo
       2: dup
       3: astore_1
       4: monitorenter  // 进入 monitor
       5: aload_1
       6: monitorexit  // 退出 monitor
       7: goto          15
      10: astore_2
      11: aload_1
      12: monitorexit  // 退出 monitor
      13: aload_2
      14: athrow
      15: return
    Exception table:
       from    to  target type
           5     7    10   any
          10    13    10   any
}

上面反编码后的代码,有两个 monitorexit 指令,一个插入在异常位置,一个插入在方法结束位置。

方法

方法中的 synchronized 与代码块中实现的方式不同,方法中会添加一个叫 ACC_SYNCHRONIZED 的标志,当调用方法时,首先会检查是否有 ACC_SYNCHRONIZED 标志,如果存在,则获取 monitor 对象,调用 monitorentermonitorexit指令。

通过 javap -v -c SynchronizedMethodDemo.class 命令反编译 SynchronizedMethodDemo 类。-v参数即-verbose,表示输出反编译的附加信息。下面以反编译普通方法为例。

Classfile /E:/SynchronizedMethodDemo.class
  Last modified 2020-6-28; size 381 bytes
  MD5 checksum 55ca2bbd9b6939bbd515c3ad9e59d10c
  Compiled from "SynchronizedMethodDemo.java"
public class SynchronizedMethodDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#13         // java/lang/Object."<init>":()V
   #2 = Fieldref           #14.#15        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #16.#17        // java/io/PrintStream.println:()V
   #4 = Class              #18            // SynchronizedMethodDemo
   #5 = Class              #19            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               demo
  #11 = Utf8               SourceFile
  #12 = Utf8               SynchronizedMethodDemo.java
  #13 = NameAndType        #6:#7          // "<init>":()V
  #14 = Class              #20            // java/lang/System
  #15 = NameAndType        #21:#22        // out:Ljava/io/PrintStream;
  #16 = Class              #23            // java/io/PrintStream
  #17 = NameAndType        #24:#7         // println:()V
  #18 = Utf8               SynchronizedMethodDemo
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/System
  #21 = Utf8               out
  #22 = Utf8               Ljava/io/PrintStream;
  #23 = Utf8               java/io/PrintStream
  #24 = Utf8               println
{public SynchronizedMethodDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

  public synchronized void demo();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED     // ACC_SYNCHRONIZED 标志
    Code:
      stack=1, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: invokevirtual #3                  // Method java/io/PrintStream.println:()V
         6: return
      LineNumberTable:
        line 8: 0
        line 10: 6
}
SourceFile: "SynchronizedMethodDemo.java"

上面对代码块和方法的实现方式进行探究:

  • 代码块通过在编译后的代码中添加 monitorentermonitorexit指令。
  • 方法中通过添加 ACC_SYNCHRONIZED 标志,来决定是否调用 monitor 对象。

Java 对象头

synchronized锁的相关数据存放在 Java 对象头中。Java 对象头指的 HotSpot 虚拟机的对象头,使用 2 个字宽或 3 个字宽存储对象头。

  • 第一部分存储运行时的数据,hashCode、锁标记位、是否偏向锁、GC 分代年龄等等信息,称作为Mark Word
  • 第二部分存储对象类型数据的指针。
  • 第三部分,如果对象是数组的话,则用这部分来存储数组长度。

Java 对象头 Mark Word 存储内容:

存储内容 标志位 状态
对象的 hashCode、GC 分代年龄 01 无锁
指向栈中锁记录的指针 00 轻量级锁
指向重量级锁的指针 10 重量级锁
11 GC 标记
线程 ID、Epoch(一个时间戳)、GC 分代年龄 01 偏向锁

锁升级

synchronized 称为重量级锁,但 Java SE 1.6 为优化该锁的性能而减少获取和释放锁的性能消耗,引入 偏向锁 轻量级锁

锁的高低级别为:无锁 偏向锁 轻量级锁 重量级锁

其中锁的升级是不可逆的,只能由低往高级别升,不能由高往低降。

偏向锁

偏向锁是优化在无多线程竞争情况下,提高程序的的运行性能而使用到的锁。在 Mark Word 中存储一个值,用来标志是否为偏向锁,在 32 位虚拟机和 64 位虚拟机中都是使用一个字节存储,0 为非偏向锁,1 为是偏向锁。

当第一次被线程获取偏向锁时,会将 Mark Word 中的偏向锁标志设置为 1,同时使用 CAS 操作来记录这个线程的 ID。获取到偏向锁的线程,再次进入获取锁时,只需判断 Mark Word 是否存储着当前线程 ID,如果是,则不需再次进行获取锁操作,而是直接持有该锁。

撤销锁

如果有其他线程出现,尝试获取偏向锁,让偏向锁处于竞争状态,那么当前偏向锁就会撤销。
撤销偏向锁时,首先会暂停持有偏向锁的线程,并将线程 ID 设为空,然后检查该线程是否存活:

  • 当暂停线程非存活,则设置对象头为无锁状态。
  • 当暂停线程存活,执行偏向锁的栈,最后对象头的保存其他获取到偏向锁的线程 ID 或者转向无锁状态。

当确定代码一定执行在多线程访问中时,那么这时的偏向锁是无法发挥到优势,如果继续使用偏向锁就显得过于累赘,给系统带来不必要的性能开销,此时可以设置 JVM 参数 -XX:BiasedLocking=false 来关闭偏向锁。

轻量级锁

代码进入同步块的时候,如果对象头不是锁定状态,JVM 则会在当前线程的栈桢中创建一个 锁记录 的空间,将锁对象头的 Mark Word 复制一份到 锁记录 中,这份复制过来的 Mark Word 叫做 Displaced Mark Word。然后使用 CAS 操作将锁对象头中的Mark Word 更新为指向 锁记录 的指针。如果更新成功,当前线程则会获得锁,如果失败,JVM 先检查锁对象的 Mark Word 是否指向当前线程,是指向当前线程的话,则当前线程已持有锁,否则存在多线程竞争,当前线程会通过自旋获取锁,这里的自旋可以理解为循环尝试获取锁,所以这过程是消耗 CPU 的过程。当轻量级锁存在竞争状态并自旋获取轻量级锁失败时,轻量级锁就会膨胀为重量级锁,锁对象的 Mark Word 会更新为指向重量级锁的指针,等待获取锁的线程进入阻塞状态。

解锁

轻量级锁解锁是使用 CAS 操作将 锁记录 替换到 Mark Word 中,如果替换成功,则表示同步操作已完成。如果失败,则表示其他竞争线程尝试过获取该轻量级锁,需要在释放锁的同时,去唤醒其他被阻塞的线程,被唤醒的线程回去再次去竞争锁。

总结

通过分析 synchronized 的使用以及 Java SE 1.6 升级优化锁后的设计,可以看出其主要是解决是通过多加入两级相对更轻巧的偏向锁和轻量级锁来优化重量级锁的性能消耗,但是这并不是一定会起到优化作用,主要是解决大多数情况下不存在多线程竞争以及同一线程多次获取锁的的优化,这也是根据平时在编码中多观察多反思得出的权衡方案。

推荐阅读

《volatile 手摸手带你解析》

《Java 线程通信之 wait/notify 机制》

《Java 多线程中使用 JDK 自带工具类实现计数器》

《Java 线程基础,从这篇开始》

退出移动版