关键字:synchronized
synchronized (this) {
// todo
}
synchronized (Demo.class) {
// todo
}
# 注意点
- 自动释放锁,即便是抛出异常。
- 实例同步方法锁是实例对象,静态同步方法锁是本类对象。
# 原理分析
# 加锁和释放锁的原理
源码:
public class SynchronizedDemo {
Object object = new Object();
public void method1() {
synchronized (object) {
}
method2();
}
private static void method2() {
}
}
编译成字节码、反编译字节码:
javac SynchronizedDemo.java
javap -verbose SynchronizedDemo.class
反编译的输出:
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: invokestatic #4 // Method method2:()V
20: return
每一个对象在同一时间只与一个 monitor (锁) 相关联,而一个 monitor 在同一时间只能被一个线程获得
monitorenter(锁 - 进入):顾名思义,进入锁,会有以下 3 种情况:
- 首次进入(获得锁):monitor 计数器为 0,本线程把锁计数器 + 1,表示本线程进入同步代码块了(获得锁)。
- 再次进入(重入锁):如果本线程已获得锁的所有权,再获取锁,表示重入,计数器 + 1。
- 进不去(未获得锁):monitor 计数器不为 0,表示锁被其他线程获取,需等待其他线程锁释放。
monitorexit(锁 - 退出):顾名思义,退出锁,执行一次,锁计数器 - 1,会有以下 2 种情况:
- -1 后不等于 0:退出的是重入锁。
- -1 后等于 0:当前线程不再拥有该 monitor 的所有权,即释放锁。
monitorenter时,如何知道当前线程是否对锁对象拥有所有权?
HotSpot 虚拟机中,monitor 是由 ObjectMonitor 实现,使用 C++ 编写实现,具体代码在 HotSpot 虚拟机源码 ObjectMonitor.hpp (opens new window) 文件中。查看源码会发现,主要的属性有_count (记录该线程获取锁的次数)、_recursions (锁的重入次数)、_owner (指向持有 ObjectMonitor 对象的线程)、_WaitSet (处于 wait 状态的线程集合)、_EntryList (处于等待锁 block 状态的线程队列)。
通过 owner
属性便知道当前线程对锁对象拥有所有权。
笔记
Synchronized 原理与 ReentrantLock 原理类似,一个在 JVM 字节码实现,一个在 Java 逻辑实现。
# JVM 中锁的优化
简单来说在 JVM 中 monitorenter 和 monitorexit 字节码依赖于底层的操作系统的 Mutex Lock 来实现的,但是由于使用 Mutex Lock 需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;
JDK1.6 中对锁的实现引入了大量的优化:
锁粗化(Lock Coarsening)
:减少不必要的紧连在一起的unlock
,lock
操作,将多个连续的锁扩展成一个范围更大的锁。锁消除(Lock Elimination)
:通过运行时 JIT 编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本的 Stack 上进行对象空间的分配 (同时还可以减少 Heap 上的垃圾收集开销)。轻量级锁(Lightweight Locking)
:这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态 (即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在 monitorenter 和 monitorexit 中只需要依靠一条 CAS 原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行 CAS 指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒 (具体处理步骤下面详细讨论)。偏向锁(Biased Locking)
:是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的 CAS 原子指令,因为 CAS 原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。适应性自旋(Adaptive Spinning)
:当线程在获取轻量级锁的过程中执行 CAS 操作失败时,在进入与 monitor 相关联的操作系统重量级锁 (mutex semaphore) 前会进入忙等待 (Spinning) 然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该 monitor 关联的 semaphore (即互斥锁) 进入到阻塞状态。
笔记
一切的优化都是为了把性能发挥到极致 ——@NipGeihou
# 锁升级
在 JDK 1.6 里 Synchronied 同步锁,一共有四种状态: 无锁
、 偏向锁
、 轻量级锁
、 重量级锁
,它会随着竞争情况逐渐升级。锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率。
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
# 自旋锁与自适应自旋锁
# 自旋锁
在「Java 中各种锁的概念」有详细讲解,此处不再重复。
存在的问题:如果线程锁在线程自旋刚结束就释放掉了锁,那么是不是有点得不偿失。所以这时候我们需要更加聪明的锁来实现更加灵活的自旋。来提高并发的性能。(于是就有了下面的自适应自旋锁)
# 自适应自旋锁
在 JDK 1.6 中引入了自适应自旋锁。这就意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。
如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么 JVM 会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到 100 次循环。相反,如果对于某个锁,自旋很少成功获取锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,JVM 对程序的锁的状态预测会越来越准确,JVM 也会越来越聪明。