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
对象,代码块的 {}
中会插入 monitorenter
和 monitorexit
指令。当执行 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
对象,调用 monitorenter
和 monitorexit
指令。
通过 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"
上面对代码块和方法的实现方式进行探究:
monitorenter
和 monitorexit
指令。 ACC_SYNCHRONIZED
标志,来决定是否调用 monitor
对象。 synchronized
锁的相关数据存放在 Java 对象头中。Java 对象头指的 HotSpot 虚拟机的对象头,使用2个字宽或3个字宽存储对象头。
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设为空,然后检查该线程是否存活:
当确定代码一定执行在多线程访问中时,那么这时的偏向锁是无法发挥到优势,如果继续使用偏向锁就显得过于累赘,给系统带来不必要的性能开销,此时可以设置 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 升级优化锁后的设计,可以看出其主要是解决是通过多加入两级相对更轻巧的偏向锁和轻量级锁来优化重量级锁的性能消耗,但是这并不是一定会起到优化作用,主要是解决大多数情况下不存在多线程竞争以及同一线程多次获取锁的的优化,这也是根据平时在编码中多观察多反思得出的权衡方案。