高并发编程-锁优化详解

152 阅读6分钟
原文链接: mp.weixin.qq.com

Java在语言上支持了锁的特性,在很多常用类的实现中也使用了锁,对于Java开发者来说就可以很方便的使用这些锁及常用类。但是,随着锁的频繁使用及错用,随之而来的就是程序执行效率变低、应用变的缓慢。为了提高线程对共享数据访问的效率,HotSpot虚拟机从JDK1.5到JDK1.6做了重大改进,提供了很多锁优化技术,包括自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。

自旋锁

线程的执行是通过竞争获取处理器的执行时间才执行的。当线程挂起或恢复执行的时,会从用户态转入内核态中完成,这种操作是很消耗时间的,在并发情况下对应用和系统来说都有很大压力。HotSpot虚拟机的开发人员注意到很多应用对共享数据锁定的时间都是很短暂的,为了这短暂的时间而挂起和恢复线程是不值得的。所以,线程并发请求锁的时候,让后来的线程在不放弃处理器执行时间的情况下稍等一下,线程做自旋,自旋期间观察持有锁的线程是否会很快释放锁,这种技术就是所谓的自旋锁。

自旋锁在JDK1.6中是默认开启的,默认自旋次数是10次,可以使用参数-XX:PreBlockSpin修改默认值。虽然,自旋锁避免了线程挂起和恢复的开销,但是它占用了处理器的执行时间,如果锁占用时间很短,自旋锁效果很好,否则会浪费处理器的执行时间,影响应用的整体性能。

自适应自旋锁

在JDK1.6中引入了自适应自旋锁,自旋的次数由上一次在同一个锁上自旋的时间和锁持有者的状态来决定。如果上一次同一个锁通过自旋刚刚被获取,并且持有锁的线程正在运行,那么虚拟机认为本次自旋也会成功,将会自旋相对长的时间获取锁。如果同一个锁很少通过自旋成功被获取,那么虚拟机认为本次自旋也会失败,不会执行自旋操作。

锁消除

一些使用了锁控制的代码,在虚拟机即时编译器运行时检测到不存在对共享数据的竞争访问,也就是代码只会被一个线程访问,此时会对锁进行消除,这项优化称为锁消除。锁消除的主要判断依据来源于逃逸分析(即分析对象的动态作用域,一个对象在方法内被定义后,在别的方法或线程中无法通过任何途径访问到这个对象,则可以进行一些优化操作)的数据支持。

锁粗化

大多数情况下,为了提高程序的执行效率,会缩小锁作用的范围。但是,对于一些连续操作都对同一个对象进行反复加锁、释放锁的情况来说,缩小锁的作用范围会消耗更多的资源,这种情况需要扩大锁的作用范围,这项优化称为锁粗化。

轻量级锁

了解轻量级锁之前,先了解一下Java对象在内存中存储的数据结构。在HotSpot虚拟机中,Java对象在内存中存储的布局分为3块区域:对象头、实例数据和对齐填充。对象头包含两部分,第一部分包含对象的HashCode、分代年龄、锁标志位、线程持有的锁、偏向线程ID等数据,这部分数据的长度在32位和64位虚拟机中分别为32bit和64bit,官方称为Mark World。考虑到虚拟机的空间效率,Mark World内部的数据结构是非固定的,也就是说对象头中存储的内容是不固定的,下图展示了不同状态下,对象头中存储的内容:

当代码执行到同步代码时,如果此时对象的锁未被锁定(锁标志位位01),虚拟机将在当前线程的栈帧中创建一个名为Lock Record空间,这个空间用于存储当前对象的Mark World拷贝,具体如下图所示。

接着,虚拟机使用CAS尝试将对象的对象头Mark Wolrd指向Lock Record,也就是在Mark Wolrd的30bit存储Lock Record的起始地址,具体如下图所示。如果上述操作执行成功,当前线程就持有了对象的锁,此时对象处于轻量级锁锁定状态,对应的锁标志位为00。

如果上述操作执行失败,首先会检查对象的对象头Mark World是否指向了当前线程栈帧中的Lock Record,如果指向了则表示当前线程已经持有了对象的锁,否则表示对象的锁已经被其它线程持有,锁膨胀为重量级锁,线程挂起等待。

轻量级锁的释放过程,通过CAS将Lock Record中存储的Mark Wolrd拷贝替换回对象的对象头Mark Wolrd中,替换成功则锁释放成功,否则表示有其它线程尝试获取过锁,释放锁的同时,唤醒挂起的线程,这里笔者的理解是此时锁膨胀为重量级锁,唤醒等待线程竞争。

偏向锁

锁对象第一次被线程持有的时候,虚拟机通过CAS把获取到这个锁的线程ID记录到对象头Mark World中,操作成功则成功获取偏向锁,对象头中的锁标志位设置为01。持有偏向锁的线程每次执行到这段同步代码时,不需要任何同步操作,这项优化称为偏向锁。

当有其它线程尝试获取对象的锁时,终止偏向模式,同时根据锁是否处于锁定状态,撤销偏向锁恢复到未锁定或轻量级锁状态。

往期精彩内容


觉得有收获,诚邀关注、点赞、转发