Synchronized加锁还可以优化?

234 阅读4分钟

我正在参与掘金创作者训练营第 4 期,点击了解活动详情,一起学习吧!

前言

前文讲述了java 锁在实现过程中,主要是锁住了对象的对象头,通过改变对象头中的关键字信息,从而获取当前共享资源的锁竞争情况。前文讲述了是加锁过程中对象头的变化情况,本文将从 Synchronized 关键字实现加锁的过程来分享,通常在加锁的过程中不仅存在锁升级,还通常伴随着锁粗化,此外还存在锁消除的情况。

Synchronized 锁实现

我们都知道,Synchronized关键字的实现由一个监视器,如果查看过编译后的 class 文件,会发现 monitor.entermonitor.exit两个指令,这就是加锁和解锁的重要组成部分。关于重量级锁阶段的加锁流程,monitor 控制线程的方式如下图所示:

对于 synchronized 的工作流程入上图所示:

  • 1 在多个线程访问一个共享资源时,先调用 monitor.enter 方法进 _EntryList 队列,该队列中的线程都是处于 blocking 状态。
  • 2 当队列中的一个线程获取到了实例对象的锁,即监视器(monitor),此时线程就可以进入运行状态(running),执行方法去做一些业务,与此同时 ObjectMonitor 对象的 _owner 指向当前线程,_count 加 1 表示当前对象锁被一个线程获取,在解锁时对应的 count 需要减 1,相反在进入方法时就会再加 1,synchronized 是可重入锁,当 _count 为 0 时,就相当于是释放了锁。
  • 3 当运行(running)状态时的线程调用 wait()方法后就会释放 monitor 对象,最后进入 waiting 状态。ObjectMonitor 对象的 _owner 变为 null,线程进入_WaitSet 队列,最后_count 减 1。直到有线程调用 notify() 方法唤醒该线程,则该线程重新获取 monitor 对象进入 _Owner 再继续操作。
  • 4 最后当线程执行完毕后释放 monitor 对象,进入 waiting 状态,ObjectMonitor 对象的 _owner 变为 null ,_count 减 1。

锁粗化

对于使用 java 关键字 synchronized 来说,不论是作用于方法还是代码块,其作用范围都应该尽可能的小,应该只在使用共享数据时才使用同步,这样做的目的是为了使同步的操作数量尽可能的小,缩短阻塞的时间。只有这样才能在存在锁竞争的情况下,尽可能快的拿到锁提高效率。

加锁是需要消耗资源的,如果存在一系列的连续加锁和解锁的操作,可能会导致不必要的性能损耗。锁粗化就是解决这样的问题,将多个连续的加锁和解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁带来的额外损耗。

锁消除

前文已经讲到了在加锁和解锁的阶段,不仅会浪费时间还会产生额外的性能开销,那么有没有办法解决这样的问题呢,这就引出了锁消除。如果一个方法在操作对象时,根本不会产生锁竞争,那么就没有必要进行加锁。在 jvm 中堆是属于共享的区域,只有栈才是线程独有的,那么如何对象只在栈上进行分配的话,那么就可以实现了,关于对象的如何在栈上进行分配,可以了解一下栈上分配。

开启栈上分配后对象就在栈上进行创建,既然在栈上创建,就可以使用一组变量来替代类信息,这就引出来了标量替换。不过栈上分配的前提是需要开启逃逸分析。

# jvm 的配置项,+ 代表启用,- 代表禁用
# 开启逃逸分析或者关闭逃逸分析
-XX:+DoEscapeAnalysis
# 开启标量替换,允许对象在栈上进行创建
-XX:+EliminateAllocations

总结

本文介绍了synchronized 加锁和解锁的流程,继续延深介绍了锁粗化和锁消除的内容,特别是锁消除的部分,需要先了解一下栈上分配的知识才能理解(通常我们认为对象都是在堆上进行存储,实际情况是在栈上也可以创建对象)。