synchronized内置锁是如何实现的?
本文基于 JDK1.8 编写。
1. 背景
当共享资源被多个线程同时访问时,可能会产生不符合预期的并发问题。为了保证共享资源在某一时刻只能被一个线程访问到,则需要对资源进行加锁,为此Java提供了一种易用的内置锁synchronized
[1]。锁的添加和释放过程如下:
- 在访问共享资源前,线程需要获取锁。如果获取锁成功,则访问共享资源;如果获取锁失败,则阻塞该线程,等待其他线程释放锁之后重新尝试获取锁。
- 在访问共享资源后,持有锁的线程需要释放锁,以供其他的线程来获取锁。
2. synchronized解析
2.1. synchronized如何使用?
synchronized
关键字有以下几种用法:
public class SynchronizedUseTest {
final Object lock = new Object();
public void lockOnCodeBlock() {
synchronized (lock) {
System.out.println("Use synchronized on code block");
}
}
public synchronized void lockOnNonStaticMethod() {
System.out.println("Use synchronized on non-static method");
}
public synchronized void lockOnStaticMethod() {
System.out.println("Use synchronized on static method");
}
}
2.2. synchronized是如何生效的?
通过反编译上面示例代码的Class
文件,我们可以得到字节码指令:
- 从第41行和第45行可以看到,
lockOnCodeBlock
方法使用monitorenter
[4]添加锁,使用monitorexit
[5]释放锁[6]。 - 从第71行可以看到,
lockOnNonStaticMethod
方法标识了ACC_SYNCHRONIZED
[7],隐式地完成锁的添加和释放。 - 从第87行可以看到,
lockOnStaticMethod
方法标识了ACC_SYNCHRONIZED
[7],隐式地完成锁的添加和释放。
无论是monitorenter
和monitorexit
,还是ACC_SYNCHRONIZED
,底层都是使用监视器(Monitor)来实现锁的添加和释放。
...
{
final java.lang.Object lock;
descriptor: Ljava/lang/Object;
flags: ACC_FINAL
public com.remeio.upsnippet.concurrency.sync.SynchronizedUseTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field lock:Ljava/lang/Object;
15: return
LineNumberTable:
line 3: 0
line 5: 4
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcom/remeio/upsnippet/concurrency/sync/SynchronizedUseTest;
public void lockOnCodeBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #5 // String Use synchronized on code block
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any
LineNumberTable:
line 8: 0
line 9: 7
line 10: 15
line 11: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 this Lcom/remeio/upsnippet/concurrency/sync/SynchronizedUseTest;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 20
locals = [ class com/remeio/upsnippet/concurrency/sync/SynchronizedUseTest, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void lockOnNonStaticMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String Use synchronized on non-static method
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/remeio/upsnippet/concurrency/sync/SynchronizedUseTest;
public synchronized void lockOnStaticMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String Use synchronized on static method
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 18: 0
line 19: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/remeio/upsnippet/concurrency/sync/SynchronizedUseTest;
}
SourceFile: "SynchronizedUseTest.java"
2.3. synchronized是如何实现内置锁的?
Java中的每一个对象都关联了一个监视器,其含有以下几个属性:
_owner
:持有锁的线程。_count
:锁重入的次数。_EntryList
:阻塞队列,用来存放阻塞(Blocked)状态的线程。_WaitSet
:等待队列,用来存放等待(Waiting)状态的线程。
synchronized
通过对监视器的操作来完成锁的添加和释放,流程如下:
当某一线程尝试获取锁时,会首先判断锁对应的监视器的
_owner
是否为空。如果
_owner
为空,则修改_owner
为当前线程,将_count
修改为1,该线程获取锁成功。如果
_owner
不为空且_owner
为当前线程,则将_count
加1,该线程重入锁成功。如果
_owner
不为空且_owner
不为当前线程,则将当前线程添加到阻塞队列_EntryList
中,等待锁的释放。
当持有锁的线程释放锁时,会将监视器的
_owner
修改为空,将_count
修改为0。锁释放后,阻塞队列_EntryList
中的线程会重新尝试获取锁。当某一线程调用
Object#wait
方法时,会释放该线程持有的锁,并将该线程添加到等待队列_WaitSet
中,等待唤醒。当Object#wait
方法超时时,会将该线程从等待队列_WaitSet
转移到阻塞队列_EntryList
中,以重新尝试获取锁。当某一线程调用
Object#notify
或Object#notifyAll
方法时,会释放该线程持有的锁,并将等待队列_WaitSet
中的线程转移到阻塞队列_EntryList
中去,以重新尝试获取锁。
3. synchronized锁升级
3.1. 为什么需要锁升级?
在JDK1.6之前的版本中,synchronized
内置锁使用的是重量级锁,每次加锁和释放锁都会调用操作系统接口,消耗性能。在JDK1.6版本中,引入了偏向锁和轻量级锁,通过锁升级的过程来适应不同的锁竞争场景,以此来优化锁的性能。锁升级依赖于对象头中的Mark Word信息:
3.2. 无锁
刚创建的对象是无锁的状态。如下示例:
public static void testNoLock() {
Object lock = new Object();
System.out.println("No lock:");
System.out.println("hashcode: " + Integer.toBinaryString(lock.hashCode()));
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
从下面的输出可以看到(输出结果中对象头按字节倒序排列):
- Mark Word为
00000000 00000000 00000000 00111111 11101110 01110011 00111101 00000001
。 - 锁标志为
01
,偏向锁标志为0
,即为无锁。 - 分代年龄为
0000
。 - 对象的
hashcode
为0111111 11101110 01110011 00111101
。
No lock
hashcode: 111111111011100111001100111101
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 3d 73 ee (00000001 00111101 01110011 11101110) (-294437631)
4 4 (object header) 3f 00 00 00 (00111111 00000000 00000000 00000000) (63)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
3.3. 偏向锁
偏向锁适用于只有一个线程竞争锁的场景。尝试获取偏向锁的线程会通过CAS切换Mark Word中的偏向线程ID:
如果切换成功,代表获取偏向锁成功。
如果切换失败,代表不止一个线程竞争锁,偏向锁会升级为轻量级锁。
获取偏向锁成功的示例如下:
public static void testBiasedLock() throws InterruptedException {
// VM flags: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
Object lock = new Object();
System.out.println("Biased lock:");
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
从下面的输出可以看到(输出结果中对象头按字节倒序排列):
- Mark Word为
00000000 00000000 00000010 00011111 10100110 00011010 10010000 00000101
。 - 锁标志为
01
,偏向锁标志为1
,即为偏向锁。 - 分代年龄为
0000
。 - Epoch偏向时间戳为
00
。 - 偏向线程ID为
00000000 00000000 00000010 00011111 10100110 00011010 100100
。
Biased lock:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 1a a6 (00000101 10010000 00011010 10100110) (-1508208635)
4 4 (object header) 1f 02 00 00 (00011111 00000010 00000000 00000000) (543)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 可以通过
-XX:+UseBiasedLocking
参数开启偏向锁,JDK1.6开始默认开启。- 可以通过
-XX:BiasedLockingStartupDelay=0
参数设置偏向锁开启的延迟时间。
3.4. 轻量级锁
轻量级锁适用于锁竞争不激烈的情况。尝试获取轻量级锁的线程会通过CAS切换Mark Word中指向线程栈中Lock Record锁记录的指针:
- 如果切换成功,代表获取轻量级锁成功。
- 如果切换失败,该线程会自适应自旋等待重新获取轻量级锁;当自旋超过一定的次数仍未获取到锁时,轻量级锁会升级为重量级锁。
获取轻量级锁成功的示例如下:
public static void testLightLock() {
Object lock = new Object();
System.out.println("Light lock:");
new Thread(() -> {
synchronized (lock) {
try {
// 模拟锁竞争不激烈的情况
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
new Thread(() -> {
// Spinning
synchronized (lock) {
// 输出结果是否是轻量级锁取决于自适应自旋次数,这里不一定是轻量级锁
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}).start();
}
从下面的输出可以看到(输出结果中对象头按字节倒序排列):
- Mark Word为
00000000 00000000 00000000 01000001 10100010 11011111 11110001 11000000
。 - 锁标志为
00
,即为轻量级锁。 - 指向线程栈中Lock Record锁记录的指针为
00000000 00000000 00000000 01000001 10100010 11011111 11110001 110000
。
Light lock:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c0 f1 df a2 (11000000 11110001 11011111 10100010) (-1562381888)
4 4 (object header) 41 00 00 00 (01000001 00000000 00000000 00000000) (65)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
3.5. 重量级锁
重量级锁适用于锁竞争激烈的场景。重量级锁会创建监视器,未获取到锁的线程会进入阻塞队列进行阻塞。如下示例:
public static void testMutexLock() {
Object lock = new Object();
System.out.println("Mutex lock:");
new Thread(() -> {
synchronized (lock) {
try {
// 模拟锁竞争激烈的情况
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
new Thread(() -> {
// Spinning too times
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}).start();
}
从下面的输出可以看到(输出结果中对象头按字节倒序排列):
- Mark Word为
00000000 00000000 00000001 01000101 11110110 01000100 01001010 00111010
。 - 锁标志为
10
,即为重量级锁。 - 指向重量级锁的指针为
00000000 00000000 00000001 01000101 11110110 01000100 01001010 001110
。
Mutex lock:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 3a 4a 44 f6 (00111010 01001010 01000100 11110110) (-163296710)
4 4 (object header) 45 01 00 00 (01000101 00000001 00000000 00000000) (325)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
3.6. 锁升级的过程
锁升级是为了适应不同的锁竞争场景,以优化锁的性能。锁升级有以下几种:
- 无锁升级为偏向锁:当某个线程尝试获取锁且CAS切换线程ID成功时。
- 无锁升级为轻量级锁:当调用锁对象的
hashcode
方法后且某个线程尝试获取锁时(偏向锁的Mark Word中无存储hashcode
的空间,所以需要升级为轻量级锁)。 - 偏向锁升级为轻量级锁:
- 当某个线程尝试获取锁且CAS切换偏向线程ID失败时。
- 或当调用锁对象的
hashcode
方法时(偏向锁的Mark Word中无存储hashcode
的空间,所以需要升级为轻量级锁)。
- 偏向锁升级为重量级锁:当调用锁对象的
Object#wait
或Object#notify
或Object#notify
方法时(需要使用Monitor对象)。 - 轻量级锁升级为重量级锁:
- 当某个线程CAS切换线程栈中的Lock Record锁记录的指针自适应自旋超时时
- 或当调用锁对象的
Object#wait
或Object#notify
或Object#notify
方法时(需要使用Monitor对象)。
- 当释放锁后,会变为无锁状态。
3.7. 锁的比较
锁类型 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
偏向锁 | 只有一个线程竞争锁的场景 | 使用CAS性能好,无需加互斥锁 | 当多个线程竞争锁时,需要撤销偏向锁,进行锁升级,会消耗资源 |
轻量级锁 | 锁竞争不激烈的场景 | 使用CAS性能好,无需加互斥锁,能够自适应自旋 | 自旋会浪费一定的CPU资源 |
重量级锁 | 锁竞争激烈的场景 | 不会自旋浪费CPU资源 | 使用互斥锁,性能差 |
4. 参考文档
- 1.Chapter 17. Threads and Locks- 17.1. Synchronization ↩
- 2.Chapter 14. Blocks and Statements - 14.19. The synchronized Statement ↩
- 3.Chapter 8. Classes - 8.4.3.6. synchronized Methods ↩
- 4.Chapter 6. The Java Virtual Machine Instruction Set - monitorenter ↩
- 5.Chapter 6. The Java Virtual Machine Instruction Set - monitorexit ↩
- 6.Chapter 3. Compiling for the Java Virtual Machine - 3.14. Synchronization ↩
- 7.Chapter 2. The Structure of the Java Virtual Machine - 2.11.10. Synchronization ↩