一、引言
在多线程编程中,正确地管理并发是确保程序正确运行的关键。Java提供了多种同步工具,其中synchronized
关键字是最基本且最常用的同步机制之一。本文旨在深入解析synchronized
的实现原理,探讨其在不同应用场景中的使用,并通过示例让读者更好地理解其工作机制。
二、Synchronized基本概念
2.1 定义和作用
synchronized
关键字可以用来修饰方法或者代码块。在方法或代码块被执行时,它能够保证同一时刻只有一个线程执行该段代码。这一特性使得synchronized
成为实现临界区(Critical Section)和避免竞态条件(Race Condition)的简便方法。
2.2 使用方法
- 同步实例方法:锁定当前实例对象
public synchronized void method() {
// 同步代码
}
- 同步静态方法:锁定当前类的Class对象。
public static synchronized void staticMethod() {
// 同步代码
}
- 同步代码块:指定一个特定对象作为锁。
public void method() {
synchronized(this) {
// 同步代码
}
}
2.3 对比其他关键字
与volatile
和final
相比,synchronized
不仅能保证可见性和顺序性,还能保证原子性。volatile
仅保证变量的修改可见性和禁止指令重排序,而final
关键字则用于声明常量。
三、Synchronized的内部机制
3.1 Java内存模型(JMM)
JMM处理了变量的可见性、原子性问题,为开发者屏蔽了不同CPU的复杂性。它确保一个线程对共享变量的修改,能够被其他线程看到,是通过内存屏障实现的。
3.2 锁的状态
在 JDK 6 中虚拟机团队对锁进行了重要改进,优化了其性能引入了 偏向锁、轻量级锁、适应性自旋、锁消除、锁粗化等实现,其中 锁消除和锁粗化本文不做详细讨论其余内容我们将对其进行逐一探究。
总体上来说锁状态升级流程如下:
- 无锁状态
- 偏向锁:假定锁不会存在竞争,避免了大多数情况下的同步。
- 轻量级锁:当锁是偏向锁时,被另一个线程访问,会升级为轻量级锁。
- 重量级锁:多线程竞争激烈时,轻量级锁会升级为重量级锁。
3.3 各种锁的获取流程
偏向锁
流程
当线程访问同步块并获取锁时处理流程如下:
- 检查 mark word 的线程 id 。
- 如果为空则设置 CAS 替换当前线程 id。如果替换成功则获取锁成功,如果失败则撤销偏向锁。
- 如果不为空则检查 线程 id为是否为本线程。如果是则获取锁成功,如果失败则撤销偏向锁。
持有偏向锁的线程以后每次进入这个锁相关的同步块时,只需比对一下 mark word 的线程 id 是否为本线程,如果是则获取锁成功。
如果发生线程竞争发生 2、3 步失败的情况则需要撤销偏向锁。
偏向锁的撤销
- 偏向锁的撤销动作必须等待全局安全点
- 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
- 撤销偏向锁恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态
优点
只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。
缺点
如果存在竞争会带来额外的锁撤销操作。
轻量级锁
加锁
多个线程竞争偏向锁导致偏向锁升级为轻量级锁
- JVM 在当前线程的栈帧中创建 Lock Reocrd,并将对象头中的 Mark Word 复制到 Lock Reocrd 中。(Displaced Mark Word)
- 线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向 Lock Reocrd 的指针。如果成功则获得锁,如果失败则先检查对象的 Mark Word 是否指向当前线程的栈帧如果是则说明已经获取锁,否则说明其它线程竞争锁则膨胀为重量级锁。
解锁
- 使用 CAS 操作将 Mark Word 还原
- 如果第 1 步执行成功则释放完成
- 如果第 1 步执行失败则膨胀为重量级锁。
优点
其性能提升的依据是对于绝大部分的锁在整个生命周期内都是不会存在竞争。在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
缺点
在有多线程竞争的情况下轻量级锁增加了额外开销。
自旋锁
自旋是一种获取锁的机制并不是一个锁状态。在膨胀为重量级锁的过程中或重入时会多次尝试自旋获取锁以避免线程唤醒的开销,但是它会占用 CPU 的时间因此如果同步代码块执行时间很短自旋等待的效果就很好,反之则浪费了 CPU 资源。默认情况下自旋次数是 10 次用户可以使用参数 -XX : PreBlockSpin 来更改。那么如何优化来避免此情况发生呢?我们来看适应性自旋。
适应性自旋锁
JDK 6 引入了自适应自旋锁,意味着自旋的次数不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果对于某个锁很少自旋成功那么以后有可能省略掉自旋过程以避免资源浪费。有了自适应自旋随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。
优点
竞争的线程不会阻塞挂起,提高了程序响应速度。避免重量级锁引起的性能消耗。
缺点
如果线程始终无法获取锁,自旋消耗 CPU 最终会膨胀为重量级锁。
重量级锁
在重量级锁中没有竞争到锁的对象会 park 被挂起,退出同步块时 unpark 唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。
ObjectMonitor
中包含一个同步队列(由 _cxq
和 _EntryList
组成)一个等待队列( _WaitSet
)。
- 被notify或 notifyAll 唤醒时根据 policy 策略选择加入的队列(policy 默认为 0)
- 退出同步块时根据 QMode 策略来唤醒下一个线程(QMode 默认为 0)
这里稍微提及一下管程这个概念。synchronized
关键字及 wait
、notify
、notifyAll
这三个方法都是管程的组成部分。可以说管程就是一把解决并发问题的万能钥匙。有两大核心问题管程都是能够解决的:
- 互斥:即同一时刻只允许一个线程访问共享资源;
- 同步:即线程之间如何通信、协作。
synchronized
的 monitor
锁机制和 JDK 并发包中的 AQS
是很相似的,只不过 AQS
中是一个同步队列多个等待队列。熟悉 AQS
的同学可以拿来做个对比。
3.3 锁升级过程
锁的升级是自动的,以减少锁的开销。例如,从偏向锁到轻量级锁的升级发生在第一个获取偏向锁的线程之外的线程尝试获取这个锁时。
3.4 对象头和锁标记位
Java对象头包含了对象的运行时数据,例如哈希码、GC标记位、锁状态等。这些信息对于锁的管理至关重要。