Java中Synchronized的优化原理

咱们知道,从 JDK1.6 开始,Java 对 Synchronized 同步锁作了充分的优化,甚至在某些场景下,它的性能已经超越了 Lock 同步锁。那么就让咱们来看看,它到底是如何优化的。 git

本来的问题

Synchronized是基于底层操做系统的 Mutex Lock 实现的,每次获取锁和释放锁的操做都会带来用户态内核态的切换,从而增长系统性能开销。github

所以,在锁竞争激烈的状况下,Synchronized同步锁在性能上就表现得很是糟糕,它也常被你们称为重量级锁安全

到了 JDK1.5 版本,并发包中新增了 Lock 接口来实现锁功能,它提供了与 Synchronized 关键字相似的同步功能,只是在使用时须要显示获取锁和释放锁。多线程

在单个线程重复申请锁的状况下,JDK1.5 版本的 Lock 性能要比 Synchronized 锁的性能好不少,也就是当时的 Synchronized 并不具有可重入锁的功能。并发

那么当时的 Synchronized 是怎么实现的?又为何不具有可重入的功能呢?性能

Synchronized原理

JVM 中的同步是基于进入和退出管程(Monitor)对象实现的。每一个对象实例都会有一个 Monitor,Monitor 能够和对象一块儿建立、销毁。优化

当多个线程同时访问一段同步代码时,多个线程会先被存放在EntryList集合(也可称为阻塞队列)中,处于BLOCKED状态的线程,都会被加入到该列表。spa

接下来当线程获取到对象的 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 锁。若是你有什么想法,欢迎在下方留言。

有兴趣的话能够访问个人博客或者关注个人公众号、头条号,说不定会有意外的惊喜。

death00.github.io/

相关文章
相关标签/搜索