深入了解Synchronized同步锁的优化方法

大家好,我是小水珠。

在并发编程中,多个线程访问同一个共享资源时,我们必须考虑如何维护数据的原子性。在JDK1.5之前,Java是依靠Synchronized关键字实现锁功能来做到这点的。Synchronized是JVM实现的一种内置锁,所得获取和释放是由JVM隐式实现。

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

到了JDK1.6版本后,Java对Synchronized同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了Lock同步锁。这一讲我们就来看看Synchronized同步锁究竟是通过了哪些优化,实现了性能地提升。

一 Synchronized同步锁实现原理

通常Synchronized实现同步锁的方式有两种,一种是修饰方法,一种是修饰方法块。以下就是通过Synchronized实现的两种同步方法加锁的方式:

微信图片_20220808223831.jpg

下面我们可以通过反编译看下具体的字节码的实现,运行以下反编译命令,就可以输出我们想要的字节码:

微信图片_202208082238311.jpg

通过输出的字节码,你会发现:Synchronized在修饰同步代码块时,是由monitorenter和monitorexit指令来实现同步的。进入monitorenter指令后,线程将持有Monitor对象,退出monitorexit指令后,线程将释放该Monitor对象。

微信图片_202208082238312.jpg

再来看以下同步方法的字节码,你会发现:当Synchronized修饰同步方法时,并没有发现monitorenter和monitorexit指令,而是出现了一个ACC_SYNCHRONIZED标识。 微信图片_202208082238313.jpg

JVM中的同步是基于进入和退出管程对象实现的。每个对象实例都会有一个Monitor,Monitor可以和对象一起创建和销毁。Monitor是由ObjectMonitor实现,而ObjectMonitor是由C++的ObjectMonitor.hpp文件实现,如下所示:

微信图片_202208082238314.jpg

当多个线程同时访问一段同步代码时,多个线程会先被存放在CollectionList集合中,处于block状态的线程,都会被加入到该列表。接下来当线程获取到对象的Monitor时,Monitor是依靠底层操作系统的Mutex Lock来实现互斥的,线程申请Mutex成功,则持有该Mutex,其他线程将无法获取到该Mutex,竞争失败的线程会再次进入ContentionList被挂起。

如果线程调用了wailt()方法,就会释放当前持有的Mutex,并且该线程会进入WaitSet集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放Mutex。

二 锁升级优化

java对象头

java对象头.jpg

1.偏向锁

偏向锁.jpg

2.轻量级锁

轻量级锁.jpg

3.自旋锁与重量级锁

重量级锁 (2).jpg

三 动态编译实现锁消除/锁粗化

四 减小锁粒度

分段所.jpg

五 总结

JVM在JDK1.6中引入了分级锁机制来优化Synchronized,当一个线程获取锁时,首先对象锁将成为一个偏向锁,这样做是为了优化同一线程重复获取导致的用户用户态与内核态的切换问题;其次如果有多个线程竞争锁资源,锁将会升级为轻量级锁,它适用于在短时间内持有锁,且锁有交替切换的场景;轻量级锁还使用了自旋锁来避免线程用户态与内核态的频繁切换,大大的提高了系统性能;但如果锁竞争太激烈了,那么同步锁将会升级为重量级锁。

减少锁竞争,是优化Sychronized同步锁的关键。我们应该尽量使Sychronized同步锁处于轻量级锁或偏向锁,这样才能提高Sychronized同步锁的性能;通过减小锁粒度来降低锁竞争也是一种常用的优化方法;另外我们还可以通过减少锁的持有时间来提高Sychronized同步锁在自旋时获取锁资源的成功率,避免Sychronized同步锁升级为重量级锁。

猜你喜欢

转载自juejin.im/post/7129530020581769252
今日推荐