Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized(关键字),而另一个是 JDK 实现的 ReentrantLock(类)。
应该明确一个概念,那就是 synchronized 锁的都是对象,而非代码!Java 中的每一个对象都可以作为锁。
synchronized 有四种使用方式。这四种使用方式又可以被分为两类:
1. 同步代码块
public void test() {
synchronized (this) {
// ...
}
}
上述例子中,锁是当前的实例对象。
public void test() {
final Object obj = new Object();
synchronized (obj) {
// ...
}
}
上述例子中,锁是 obj 引用的实例对象。
2. 同步非静态方法
public synchronized void test() {
// ...
}
锁是当前实例对象。
1. 同步代码块
public void test() {
synchronized (Test.class) {
// ...
}
}
锁是 Test 类的 Class 对象,此时不同线程调用同 Test 类的不同对象上的同步语句,也会进行同步。
2. 同步静态方法
public synchronized static void test() {
// ...
}
锁是 Test 类的 Class 对象,此时不同线程调用同 Test 类的不同对象上的同步语句,也会进行同步。
在讨论 synchronized 实现原理之前,首先要了解两个实现 synchronized 的基础概念:
一、Java 对象头
在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域,分别如下:
本文仅来介绍一下对象头的内容,它的结构与内容如下图所示。
如上图所示,Java 对象头里主要有两个部分(这里讨论的是非数组类型,如果是数组类型还会有第三个结构),一个 Mark Word,另一个是 Class Metadata Address,这个内容的说明在图中已经解释了,这里不多赘述。
我们主要来关注一下这个 Mark Word,它是实现锁的关键,synchronized 用的锁就是存放在这里的。
Java 对象头里的 Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。32 位 JVM 的 Mark Word 的默认存储结构如下图所示。
由于对象头的信息是与对象自己本身定义的数据没有关系的额外存储成本,因此考虑到 JVM 的空间效率,Mark Word 被设计为一个非固定的数据结构。
在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为存储以下 5 种数据。
注:上述表格中的五行,都是独立的,换句话说,Mark Word 只能处于这几种状态中的一种。
二、Monitor
每个 Java 对象天生自带了一把看不见的锁,它就是 Monitor 锁,通常 Monitor 也被描述为一个对象。
我们再回过头来看看,刚才对 Mark Word 的介绍,关注重量级锁即可,如下图所示。
注:偏向锁和轻量级锁是乐观锁,跟 Monitor 无关,所以本文所讨论的 synchronized 锁的原理就是讨论重量级锁的底层实现原理,但请明确一点,从 JDK1.6 以后,synchronized 可不仅仅是重量级的锁,它还有偏向锁和轻量级锁这两种状态
上图中所说的 “指向重量级锁的指针”,其实就是指向的 Monitor 对象的起始地址,每个对象都存在着一个 Monitor 对象与之关联。对象与其 Monitor 之间存在多种实现方式,比如在对象创建时,其 Monitor 对象随之创建,销毁时也如此,或者当线程试图获取对象锁时,自动创建 Monitor 对象。
当一个 Monitor 被某个线程持有后,它就会处于锁定状态,所谓的获取锁本质上就是获取 Monitor 对象的持有权,在 HotSpot 虚拟机中,Monitor 是由 ObjectMonitor 来实现的,其源码(C++ 实现)地址如下,:
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/36a743eee672/src/share/vm/runtime/objectMonitor.hpp
简单的来看一下 ObjectMonitor 的构造函数
我们重点关注 WaitSet、EntryList 、owner、count 这四个变量。
1. WaitSet、EntryList
这里先要额外介绍两个概念:
上图源码中圈红的:WaitSet 和 EntryList 这两个队列,就是代表某个对象的等待池和锁池。它们就是用来保存 ObjectWaiter 对象的列表,那么 ObjectWaiter 又是个啥?别急,接着往下看,ObjectWaiter 源码如下:
每一个等待该对象锁的线程都会被封装成一个 ObjectWaiter 对象。
2. owner
owner 这个字段它是指向持有 ObjectMonitor 对象的线程,也就是获得该对象锁的线程。
3. count
就是互斥量,有 0/1 两种取值,1 代表锁正被占有。
了解完了对象头和 Monitor 这两个概念之后,我们从底层原理的角度来阐述一下锁的获取与释放的具体过程。
当多个线程同时访问同一段同步代码的时候,首先它们会进入 EntryList 里面,当线程获取到该对象的 Monitor 对象后,就会进入临界区执行同步代码,并把 owner 变量设置为指向当前线程,同时 count 字段会执行加 1。
此时如果该线程调用 wait() 方法,那么它就会释放当前持有的 Monitor 对象,同时 owner 变量恢复成 null、count 也会执行减 1,并进入 WaitSet 里面等待被唤醒 。
若当前线程执行完毕,它也会释放当前持有的 Monitor 对象,同时复位 owner、count 变量,以便其他线程获取锁。
JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的,细节在 JVM 规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。
看一下代码块同步的同步实现,写一个简单的代码块同步代码如下:
public class SynchronizedTest {
public void test() {
synchronized (this) {
System.out.println("test");
}
}
}
之后,使用javac SynchronizedTest.java
指令获取编译后的字节码文件:SynchronizedTest.class
获取字节码文件之后再使用javap -verbose SynchronizedTest.class
指令反编译字节码文件来查看具体指令。结果如下所示
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
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 test
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
....
可以看出,monitorenter 指令是在编译后插入到同步代码块的开始位置(3: monitorenter),而 monitorexit 是插入到方法结束处(13: monitorexit)和异常处(19: monitorexit),第二个 monitorexit 是为了保证程序抛出异常时,依然能有一个 monitorexit 与 monitorenter 配对,以确保 Monitor 对象能够正常的释放 。