synchronized的底层实现
引言
在单机环境中的并发编程中,需要用锁来保证数据的安全性。我们经常会用到synchronized,那么JVM中是如何实现synchronized的呢,在这篇文章中,我会从锁分类和锁膨胀(锁升级)的角度,会来探析一二。
为什么要有锁对象
Object lockObj = new Object();
synchronized(lockObj){
//TODO
}
在访问某块代码或者变量时,为了防止线程安全问题,我们需要先获取锁对象,通过锁的互斥来保证线程的安全访问。在上面这块代码中,lockObj就是锁对象。
synchronized底层实现的锁分类
- 偏向锁
- 轻量级锁
- 重量级锁
在这3种分类中,偏向锁和轻量级锁是在逻辑层面做的一些处理,并非真正的锁,只有重量级锁,才是真正的锁(操作系统层面的锁)。
为什么要对锁分类
假设现在有A和B两个线程,要访问共有变量。一般来说有如下3种情况:
- 每次只有线程A或者线程B单独使用。
- 线程A和线程B交替使用。
- 线程A和线程B,同时使用。
众所周知,获取锁是比较消耗性能的。所以在Java中,synchronized提供了几种锁的实现来优化。
对于这3种情况,前两种可以在逻辑层面做一些处理,避免每次获取锁(操作系统锁),带来的性能开销。对于1,可以使用偏向锁。对于2,可以使用轻量级锁。
偏向锁
偏向锁会保证对象被线程安全的访问。
锁对象
被synchronized锁保护的,称作锁对象。锁对象中包含了锁对象头,由线程id、Epoch、分代年龄、是否偏向锁标记、锁标记组成。
线程id:每次获取锁对象时,会先检查线程id是否与当前线程一致,如果线程id是空,则通过CAS设置对象头中的线程id。
Epoch:本质是时间戳。使用Epoch通过CAS来保证设置线程id的安全性。
运行原理
在获取锁对象时,首先会检查锁对象头中的线程id是否与当前线程一致。
- 如果线程id是空,则通过CAS设置对象头中的线程id,并更新Epoch。
- 如果线程id与当前线程一致,则可以安全访问。
- 如果线程id与当前线程不一致,则需要锁膨胀。(升级为轻量级锁)
轻量级锁
在偏向锁获取不到锁对象时,会通过自旋来不断的尝试获取锁,这就称为轻量级锁。
重量级锁
在通过一定的自旋次数后,如果还获取不到锁,就会升级为重量级锁,所有获取不到锁对象的线程都会被阻塞(Blocked状态)。
重量级锁会使用Monitor获取操作系统的MutexLock(互斥锁)
什么时候切换锁类型
在默认情况下,会先尝试使用偏向锁,如果获取不到,则升级为轻量级锁,轻量级锁在一定的自旋次数后,会升级为重量级锁。获取锁是按:偏向锁->轻量级锁->重量级锁,依次升级,且无法降级。
只有当当前锁无法获取到锁对象时,才会升级。