本文着重介绍 JVM 中 synchronized 锁相关的内容。更多相关文章和其他文章均收录于贝贝猫的文章目录。
锁是多线程软件开发的必要工具之一, 它的基本作用是保护临界区资源不会被多个线程同时访问而受到破坏。如果由于多线程访问造成对象数据的不一致, 那么系统运行将会得到错误的结果。通过锁, 可以让多个线程排队, 一个一个地进入临界区访问目标对象, 使目标对象的状态总是保持一致, 这也就是锁存在的价值。
在 Java 中存在两种实现同步的语义,synchronized 方法和 synchronized 代码块:
public class Test {
public void synchronizedBlock() {
synchronized (this) {
System.out.println("synchronizedMethod");
}
}
public synchronized void synchronizedMethod() {
System.out.println("synchronizedMethod");
}
}
上述代码两个函数生成的字节码如下所示:
// synchronizedBlock
// access flags public
0 aload_0
1 dup
2 astore_1
3 monitorenter
4 getstatic #2
7 ldc #3
9 invokevirtual #4
12 aload_1
13 monitorexit
14 goto 22 (+8)
17 astore_2
18 aload_1
19 monitorexit
20 aload_2
21 athrow
22 return
// synchronizedMethod
// access flags public synchronized
0 getstatic #2
3 ldc #3
5 invokevirtual #4
8 return
从字节码中,我们会发现编译器会将同步代码块用 monitorenter
和 monitorexit
包裹,这里因为有隐式的 try-finally
所以有两个 monitorexit
,而同步方法,则是通过函数的的 access flags
进行标识,当执行有 synchronized
标识的函数时,最后也会调用 monitorenter
和 monitorexit
基本一样的逻辑。
在 JDK 1.6 之前, synchronized 只使用了系统层提供的锁机制,例如 Linux 平台上的 mutex,这会造成进程间切换的开销,所以性能并不理想。在 JDK 1.6 引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。JVM 会根据竞争的强度来选用不同的锁实现,来降低锁的使用开销。那么 JVM 是如何实现这么多种类型的锁的呢?在 JVM 的实现中,每个对象都有一个对象头,用于保存对象的系统信息。对象头中有一个成为 Mark Word 的部分,它是实现锁的关键。在 32 位系统中,它是一个 32 位的数据,在 64 位系统中,它占 64 位。它是一个多功能的数据区,可以存放对象的哈希值,对象年龄,所得指针等数据。一个对象使用了哪种锁实现,谁占用了锁,都记录在这个 Mark Word 中。下表展示了 32 位 HotSpot 虚拟机中 Mark Word 的存储情况。
可以看到锁信息也是存在于对象的 mark word 中的。当对象状态为偏向锁(biasable)时,mark word 存储的是偏向的线程 ID;当状态为轻量级锁(lightweight locked)时,mark word 存储的是指向线程栈中 Lock Record 的指针;当状态为重量级锁(inflated)时,为指向堆中的 monitor 对象的指针。接下来,我们介绍一下这三种类型的锁都具有什么样的特点,以及它们的实现方式。
在 JDK1.6 中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个 CAS 操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的 CAS 命令。
mark word
将是可偏向状态,此时的 thread id
为 0,表示当前没有任何一个线程持有该锁。mark word
确认当前锁是否处于未偏向模式,如果处于未偏向模式则会通过 CAS 指令,将 mark word
中的 thread id
0 改为当前线程 id。如果成功,则说明获得了偏向锁,继而执行同步块中的代码,否则,说明当前存在多线程竞争,不适合使用偏向锁,开始执行锁的升级逻辑,锁的升级我们后面介绍。锁升级的过程是在 JVM 中专门的 VM Thread 中执行,该线程会源源不断的从 VMOperationQueue 中取出请求,比如 GC 请求和锁升级请求都会被推入该请求队列中。对于需要 safepoint 的操作,必须要等到所有的 Java 线程进入到 safepoint 才开始执行。
理解了偏向锁的各个处理过程之后,我们来看一下下面这张图,其中展示了偏向锁状态的切换流程:
从图中最上面出发会分为偏向锁可用和不可用:
至此,上图中关于偏向锁的部分就都介绍完了,接下来,我们介绍一下轻量级锁的实现和升级流程。
JVM 的开发者发现在很多情况下,在 Java 程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此 JVM 引入了轻量级锁的概念。
线程在执行同步块之前,JVM 会先在当前的线程的栈帧中创建一个 Lock Record,其包括一个用于存储对象头中的 mark word(官方称之为 Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个 Lock Record。可以看到 Displaced Mark Word 中保存了 mark word 中原来的内容,然后 mark work 变为指向该 Lock Record 的指针。
轻量级锁的 Lock Record 和偏向锁 Lock Record 的区别就在这个 Displaced Mark Word,在偏向锁的所有 Lock Record 的 Displaced Mark Word 都为 null,而在轻量级锁中,会存在一个 Displaced Mark Word 不为空的 Lock Record。然后 mark word 中会保存一个指向该 Lock Record 的指针。而如果一个线程重复获取该锁(重入锁),后续加锁过程只会添加 Displaced Mark Word 为 null 的 Lock Record,这部分就和偏向锁一样了。看到这,想必大家已经清楚从偏向锁到轻量级锁的直接升级过程了,就是将一个 Displaced Mark Word 本为 null 的 LockRecord 更新,复制 mark word 中的内容,然后通过 CAS 将 mark word 改为指向该 Lock Record 的指针。
总结一下轻量级锁的处理流程:
重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现 Java 中的线程同步。重量级锁的状态下,对象的 mark word 为指向一个堆中 monitor 对象的指针。
一个 monitor 对象包括这么几个关键字段:ContentionList,EntryList ,WaitSet,owner。其中 ContentionList(cxq) ,EntryList ,WaitSet 都是由 ObjectWaiter 的链表结构,owner 指向持有锁的线程。
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个 ObjectWaiter 对象插入到 ContentionList 的队列,然后暂停当前线程。当持有锁的线程释放锁前,会将 ContentionList 中的所有元素移动到 EntryList 中去,并唤醒 EntryList 的队首线程。这个选中的线程叫做 Heir presumptive 即假定继承人,就是图中的 Ready Thread,假定继承人被唤醒后会尝试获得锁,但 synchronized 是非公平的,所以假定继承人不一定能获得锁,它其实会和当前正准备获得锁的线程进行争夺,这也就是为什么使用 synchronized 时,有可能最后申请锁的线程反而第一个拿到了锁。
如果一个线程在同步块中调用了 Object#wait 方法,会将该线程对应的 ObjectWaiter 从 EntryList 移除并加入到 WaitSet 中,然后释放锁。当 wait 的线程被 notify 之后,会将对应的 ObjectWaiter 从 WaitSet 移动到 EntryList 中。需要注意的是,当调用一个锁对象的 wait 或 notify 方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。因为偏向锁和轻量级锁是不具备进程切换能力的。
最后回到升级示意图中,我们可以介绍最右下角的部分了:
当轻量级锁状态发生严重的竞争时(或者调用了锁对象 wait 或 notify 方法),会创建一个 monitor 对象(需要根据当前锁的持有情况进行 monitor 的初始化),然后将 mark word 改为指向该 monitor 的指针。
[1]《实战 Java 虚拟机》
[2]《深入理解 Java 虚拟机》
[3] GC复制算法和标记-压缩算法的疑问
[4] Java中什么样的对象才能作为gc root,gc roots有哪些呢?
[5] concurrent-mark-and-sweep
[6] 关于 -XX:+CMSScavengeBeforeRemark,是否违背cms的设计初衷?
[7] Java Hotspot G1 GC的一些关键技术
[8] Java 垃圾回收权威指北
[9] [HotSpot VM] 请教G1算法的原理
[10] [HotSpot VM] 关于incremental update与SATB的一点理解
[11] Java线程的6种状态及切换
[12] Java 8 动态类型语言Lambda表达式实现原理分析
[13] Java 8 Lambda 揭秘
[14] 字节码增强技术探索
[15] 不可不说的Java“锁”事
[16] 死磕Synchronized底层实现–概论
[17] 死磕Synchronized底层实现–偏向锁
[18] 死磕Synchronized底层实现–轻量级锁
[19] 死磕Synchronized底层实现–重量级锁