前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM中的锁优化原理

JVM中的锁优化原理

原创
作者头像
LuceneReader
修改2020-06-22 18:09:22
6450
修改2020-06-22 18:09:22
举报
文章被收录于专栏:solr lucene源码解析

JVM为了提高性能,在内置锁上做了非常多的优化,理解偏向锁、轻量级锁、重量级锁要解决的问题,几种锁的分配和膨胀过程,有助于理解和优化基于锁的并发程序。

内置锁是JVM提供的最便捷的线程同步工具,在代码块和方法声明上添加synchronized关键字即可使用内置锁,使用内置锁可以简化并发模型,随着JVM的升级,不用修改代码,即可享受到JVM内置锁的优化成果。从简单的重量级锁,到逐渐膨胀的锁分配策略,使用了多种优化手段解决隐藏在内置锁下的基本问题。

重量级锁:

内置锁在Java中被抽象为监视器锁(monitor),在JDK1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex),

这种同步方式的成本非常高,包括系统调用引起的内核态和用户态的切换,线程阻塞造成的线程切换,因此,这种锁被称为"重量级锁"。

自旋锁:

用户态和内核态切换不容易优化,通过自旋锁,可以减少线程阻塞造成的线程切换(挂起线程和恢复线程),如果锁的粒度小,那么锁的持有时间比较短,那么对于竞争这些锁而言,因为锁阻塞造成的线程切换的时间和锁的持有时间相当,减少线程阻塞造成的线程切换,能得到很大的性能提升。具体如下:

  • 当前线程竞争锁失败时,打算阻塞自己
  • 不直接阻塞自己,而是通过自旋一会(空等待)
  • 在自旋的同时,重新竞争锁
  • 如果在自旋结束前获得了锁,那么锁获取成功,否则,自旋结束后阻塞自己

如果在自旋的同时,需要竞争的锁被持有者释放了,那么当前线程不需要阻塞自己,从而减少了一次线程切换。我们可以通过-XX:-UseSpinning来关闭自旋锁的优化,-XX:PreBlockSpin参数修改默认的自旋次数。

自适应自旋锁:

自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。

  • 如果在同一个锁对象上,自旋等待刚刚成功获取到锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而他将允许自旋等待持续更长的时间
  • 相反的,如果对于某个锁,自旋很少成功获取过,那么在以后要获取这个锁时,将减少自旋时间甚至省略自旋过程。

自适应自旋解决的是"锁竞争时间不固定"的问题,自适应自旋假设不同的线程持有同一个锁的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次的自旋时间和结果调整下一次的自旋的时间。

轻量级锁:

自旋锁的目标是减少线程切换的成本,如果锁竞争激烈,我们不得不依赖重量级锁,让竞争失败的线程阻塞,如果完全没有实际的锁竞争,那么申请重量级锁是浪费的,轻量级锁的目标是,减少无实际竞争的情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态的切换,线程阻塞引起的线程切换。

轻量级锁时相对于重量级锁而言的,使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,那么轻量级锁获取成功,记录锁状态为轻量级锁,否则说明,已经有线程获取了轻量级锁,接下来膨胀为重量级锁。如果竞争激烈,那么轻量级锁很快膨胀为重量级锁,那么维持轻量级锁的过程就造成了浪费。

偏向锁:

如果没有实际的竞争,并且自始至终,使用锁的线程只有一个,维护轻量级锁都是浪费的,偏向锁的目标是,减少无竞争,并且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗,轻量级锁每次申请和释放都至少需要一次CAS,但偏向锁只有初始化时,需要一次CAS。偏向锁假定将来只有第一个申请锁的线程使用锁,因此只需要在Mark Word的CAS记录owner,如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner,就可以零成本的直接获取锁,否则,说明存在锁竞争,膨胀为轻量级锁。可以通过-XX:-UseBiasedLocking禁止偏向锁优化。

偏向锁,轻量级锁,重量级锁适用于不同的并发场景:

  • 偏向锁 无实际竞争,将来只有第一个申请锁的线程会使用锁
  • 轻量级锁 无实际竞争,多个线程交替使用锁,允许短时间的锁竞争
  • 重量级锁 有实际竞争,锁竞争时间长

下图为锁的分配和膨胀流程图:

同步的原理:

JVM规范规定,JVM是基于进入和退出Monitor对象来实现方法的同步和代码块的同步,代码块同步是使用monitorenter和monitorexit指令实现,monitorenter是编译后插入到同步代码块的开始位置,而monitorexit插入到方法的结束处或者异常处。

volatile实现的原理:

多线程编程中,Volatile用来保证共享变量的可见性,当一个线程修改这个共享变量时,另外的线程能读到这个修改的值。Java编程允许线程访问共享变量,为了确保共享变量被准确一致的更新,一个变量被声明为volatile,Java内存模型将保证所有的线程看到的这个变量的值是一致的。用volatile修饰的变量在进行写操作时,会执行lock指令,该指令在x86处理器上会将处理器缓存行的数据写回到系统内存,写回内存的操作引起其他CPU里缓存了该内存地址的数据无效。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档