我们知道,从 JDK1.6 开始,Java 对 Synchronized 同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了 Lock 同步锁。那么就让我们来看看,它究竟是如何优化的。
<!-- more -->
原本的问题
Synchronized
是基于底层操作系统的 Mutex Lock 实现的,每次获取锁和释放锁的操作都会带来用户态
和内核态
的切换,从而增加系统性能开销。
因此,在锁竞争激烈的情况下,Synchronized
同步锁在性能上就表现得非常糟糕,它也常被大家称为重量级锁
。
到了 JDK1.5 版本,并发包中新增了 Lock 接口来实现锁功能,它提供了与 Synchronized 关键字类似的同步功能,只是在使用时需要显示获取锁和释放锁。
在单个线程重复申请锁的情况下,JDK1.5 版本的 Lock 性能要比 Synchronized 锁的性能好很多,也就是当时的 Synchronized 并不具备可重入锁
的功能。
那么当时的 Synchronized 是怎么实现的?又为什么不具备可重入的功能呢?
Synchronized原理
JVM 中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 可以和对象一起创建、销毁。
当多个线程同时访问一段同步代码时,多个线程会先被存放在EntryList集合
(也可称为阻塞队列
)中,处于BLOCKED
状态的线程,都会被加入到该列表。
接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex。
如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入WaitSet集合
(也可称为等待队列
)中,等待下一次被唤醒。此时线程会处于WAITING
或者TIMEDWAITING
状态,
如果当前线程顺利执行完方法,也将释放 Mutex。
总的来说,就是同步锁在这种实现方式中,因 Monitor 是依赖于底层的操作系统实现,存在用户态
与内核态
之间的切换(可以理解为上下文切换
),所以增加了性能开销。
锁升级
为了提升性能,JDK1.6 引入了偏向锁、轻量级锁、重量级锁概念,来减少锁竞争带来的上下文切换,而正是新增的Java对象头
实现了锁升级
功能。
所谓锁升级
,就是指
Synchronized 同步锁初始为偏向锁
,随着线程竞争越来越激烈,偏向锁
升级到轻量级锁
,最终升级到重量级锁
。
偏向锁
偏向锁
主要用来优化同一线程多次申请同一个锁的竞争,也就是现在的Synchronized锁
实际已经拥有了可重入锁的功能。
为什么要有偏向锁
?因为在我们的应用中,可能大部分时间是同一个线程竞争锁资源(比如单线程操作一个线程安全的容器),如果这个线程每次都要获取锁和释放锁,那么就在不断的从内核态
与用户态
之间切换。
那么有了偏向锁
,当一个线程再次访问这个同步代码或方法时,该线程只需去对象头中去判断一下是否当前线程是否持有该偏向锁就可以了。
一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点
(JVM的stop the world
),暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占。
轻量级锁
当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换对象头中的线程 ID 为自己的 ID,该锁会保持偏向锁
状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁
。
轻量级锁
适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。
轻量级锁也支持自旋
,因此其他线程再次争抢时,如果CAS
失败,将不再会进入阻塞状态
,而是不断自旋。
之所以自旋更好,是因为之前说了,默认线程持有锁的时间都不会太长,如果线程被挂起阻塞可能代价会更高。
如果自旋锁重试之后抢锁依然失败,那么同步锁就会升级至重量级锁
。
重量级锁
在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在WaitSet集合
中,也就变成了优化之前的Synchronized锁
。
JVM参数优化
偏向锁
升级为轻量级锁
时,会发生stop the world
,如果系统常常是多线程竞争,那么禁止偏向锁也许是更好的选择,可以通过以下JVM参数
进行优化:
// 关闭偏向锁(默认打开)
-XX:-UseBiasedLocking
// 设置重量级锁
-XX:+UseHeavyMonitors
轻量级锁
拥有自旋锁
的功能,那么如果线程持有锁的时间很长,那么竞争的线程也会常常处于自旋状态,占用系统 CPU ,增加系统开销,那么此时关闭自旋锁的优化可以更好一些:
-XX:-UseSpinning
总结
以上便是 Java 中针对 Synchronized 锁的优化,也正是因为这个优化,ConcurrentHashMap 在 JDK1.8 之后,再次采用 Synchronized 锁。如果你有什么想法,欢迎在下方留言。
有兴趣的话可以访问我的博客或者关注我的公众号、头条号,说不定会有意外的惊喜。