高效并发:Synchornized的锁优化详解

  • 1. HotSpot虚拟机的对象头的内存布局
  • 2. 偏向锁
    • 举一反三:当锁进入偏向状态时,存储hash码的位置被覆盖了,那对象的hash码存储到哪儿的?
  • 3. 自旋锁与自适应自旋
  • 4. 轻量级锁
  • 5. 重量级锁
  • 6. 各种锁的优缺点以及使用场景
  • 7. 一张图读懂synchornized锁膨胀的过程

高效并发是从JDK 5升级到JDK 6后一项重要的改进项,HotSpot虚拟机开发团队在这个版本上花费了大量的资源去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(LockElimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(BiasedLocking)等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程的执行效率

JDK 6之前,Synchornized关键字无论什么情况都会去使用ObjectMonitor对象来实现多线程。
JDK 6之后,Synchornized具有无锁 -> 偏向锁 -> 轻量级锁(采用自适应自旋) -> 重量级锁的优化过程

我个人认为:除了重量级锁,其它的状态基本都是只通过锁对象的Mark Word以及CAS来帮助实现的,提升了效率。而重量级锁会利用ObjectMoniter对象去与OS层面的mutex信号量做映射,线程切换的时候也会造成更多的消耗(详细见后文)。

了解锁优化之前,必须先了解HotSpot虚拟机的对象头的内存布局,它有助于我们去了解其中的原理

1. HotSpot虚拟机的对象头的内存布局

在不同的锁状态下,Mark word会存储不同的信息,这也是为了节约内存常用的设计

  • 存储自身的运行时数据:哈希码(HashCode)、GC分代年龄(Generational GC Age) 、锁标志相关信息等。
  • 另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度。

32位的MarkWord

64位的MarkWord

2. 偏向锁

偏向锁也是JDK 6中引入的一项锁优化措施,它的目的是消除数据在无竞争情况下的同步原语, 进一步提高程序的运行性能。默认开启

偏向锁的定义:这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

偏向锁工作原理

  • 当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。

    如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)。

  • 一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束,等待全局安全点时(不执行字节码指令的时候),开始撤销偏向锁,根据锁对象目前是否处于被锁定的状态发生以下两种情况:

    • 撤销偏向锁后标志位恢复到未锁定
    • 变为轻量级锁定(标志位为“00”)的状态

  • 后续的同步操作就按照马上介绍的轻量级锁那样去执行。

举一反三:当锁进入偏向状态时,存储hash码的位置被覆盖了,那对象的hash码存储到哪儿的?

关于hash:在Java语言里面一个对象如果计算过哈希码,就应该一直保持该值不变,否则很多依赖对象哈希码的API都可能存在出错风险。绝大多数对象哈希码来源的**Object::hashCode()**方法,返回的是对象的一致性哈希码。

如何保持hash码一致不变?

它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变。

因此,由于偏向锁的产生需要利用存储hash码的位置,所以当一个对象计算过hash码后会造成下面两种情况:

  • 当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了

  • 当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁

    这里说的计算请求应来自于对Object::hashCode()或者System::identityHashCode(Object)方法的 调用,如果重写了对象的hashCode()方法,计算哈希码时并不会产生这里所说的请求

在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态(标志位为“01”)下的Mark Word,其中自然可以存储原来的哈希码。

偏向锁可以提高带有同步但无竞争的程序性能。但如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。

3. 自旋锁与自适应自旋

自旋锁:如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

JDK 6中默认开启,轻量级锁状态下会使用自适应自旋锁去尝试获取锁,所以我们先介绍自旋锁与自适应自旋的概念。

自旋锁的缺点:

  • 如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有价值的工作,这就会带来性能的浪费。

解决方案:在 JDK 6中对自旋锁的优化,引入了自适应的自旋。

  • 自适应自旋:自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。

    如果对于同一个锁对象,自旋等待刚刚成功获得过锁。那么虚拟机会将自旋次数设置为更大。如果通过自旋很少获得锁,那么会减少自旋次数甚至不开启自旋以避免浪费处理器资源。

    有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,性能就会越来越好。

4. 轻量级锁

重量级锁:在传统的锁机制中,每次发生线程切换时,需要从用户态切换到核心态,然后发出中断处理并作出保护现场、恢复现场的一些操作,代价及其昂贵,因此传统的使用操作系统互斥量来实现的锁机制被称为”重量级锁“

轻量级锁的工作流程

  • 在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,存储锁对象目前的Mark Word的拷贝(官方为这份拷贝加了一个Displaced前缀,即Displaced Mark Word)

  • 然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针

    • 更新成功:如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。这时候线程堆栈与对象头的状态如下图所示

    • 更新失败:如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。

      • 当前线程重复进入:虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了
      • 其它线程抢占:对象的Mark Word已经被修改,此时轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态
  • 解锁过程同样通过CAS来实现:如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。

    • 假如能够成功替换,那整个同步过程就顺利完成了
    • 如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

5. 重量级锁

可以先看看moniter的实现原理,有助于帮助理解

Moniter的实现原理

[Java面试常见问题:Monitor对象是什么? - 知乎 (zhihu.com)]

使用了monitor的对象的就是重量级锁,因为monitor的实现依赖于底层操作系统的mutex互斥原语,而操作系统实现线程之间的切换的时候需要从用户态转到内核态,这个转成过程开销比较大。

对于monitor的引用是也是存放在对象的Mark Word中的

下面两种方式就是重量级锁:

  • 同步方法的时候

    Jvm采用的是 ACC_synchoronized 标记符来实现的同步,这个是因为jvm在调用方法时会验证方法是不是有ACC_synchoronized 的标记符。如果设置了该标志,执行线程会线获取到monitor对象,然后执行方法,在该方法的运行期间,其他线程是无法获取到monitor对象的,只有当拥有monitor对象的线程执行完任务了才能获取,释放了monitor对象才能进入到代码块。

  • 同步代码块的时候

    对于同步代码块,是由 monitorentermonitorexit指令来实现的同步。monitorenter是获取monitor的所有权,mointorexit是释放monitor的所有权。

    monitorenter的执行原理

    • 获取到monitor的所有权,进入数+1
    • 如果该线程已经拥有了此次方法的monitor,又重新进入到monitor。进入数+1这个就是锁的重入
    • 其他线程进入到阻塞状态,直到monitor的进入数为0时,才会重新获取到monitor的所有权
      monitorexit则表示该线程必须释放montor的所有权。并把进入数减去1直到为0线程退出monitor。

6. 各种锁的优缺点以及使用场景

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和非同步块只有纳秒级别的差距 如果存在线程竞争,回带来额外的锁撤销消耗 使用于一个线程访问同步块
轻量级锁 竞争的线程不会阻塞,提高来线程的响应速度 如果始终得不到锁竞争的线程,是使用自旋消耗CPU 追求响应 同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长

7. 一张图读懂synchornized锁膨胀的过程


参考资料:《Java并发编程的艺术》《深入理解JAVA虚拟机》

高效并发:Synchornized的锁优化详解相关推荐

  1. python实现单例模式的几种方式_基于Python中单例模式的几种实现方式及优化详解...

    单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场. ...

  2. 网易考拉海购Dubbok框架优化详解

    网易考拉海购Dubbok框架优化详解 摘要:微服务化是当前电商产品演化的必然趋势,网易考拉海购通过微服务化打破了业务爆发增长的架构瓶颈.本文结合网易考拉海购引用的开源Dubbo框架,分享支持考拉微服务 ...

  3. 高并发高流量网站架构详解

    (推荐)高并发高流量网站架构详解 Web2.0的兴起,掀起了互联网新一轮的网络创业大潮.以用户为导 向的新网站建设概念,细分了网站功能和用户群,不仅成功的造就了一大批新生的网站,也极大的方便了上网的人 ...

  4. mysql innodb 的锁机制_Mysql之Innodb锁机制详解

    InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION):二是采用了行级锁.关于事务我们之前有专题介绍,这里就着重介绍下它的锁机制. 总的来说,InnoDB按照不同的分类共有 ...

  5. C++11 并发指南三(std::mutex 详解)

    上一篇<C++11 并发指南二(std::thread 详解)>中主要讲到了 std::thread 的一些用法,并给出了两个小例子,本文将介绍 std::mutex 的用法. Mutex ...

  6. 【转】C++11 并发指南五(std::condition_variable 详解)

    http://www.cnblogs.com/haippy/p/3252041.html 前面三讲<C++11 并发指南二(std::thread 详解)>,<C++11 并发指南三 ...

  7. lucene.NET详细使用与优化详解

    lucene.NET详细使用与优化详解 http://www.cnblogs.com/qq4004229/archive/2010/05/21/1741025.html http://www.shan ...

  8. C++11 并发指南五(std::condition_variable 详解)

    前面三讲<C++11 并发指南二(std::thread 详解)>,<C++11 并发指南三(std::mutex 详解)>分别介绍了 std::thread,std::mut ...

  9. java一个方法排他调用_Java编程实现排他锁代码详解

    一 .前言 某年某月某天,同事说需要一个文件排他锁功能,需求如下: (1)写操作是排他属性 (2)适用于同一进程的多线程/也适用于多进程的排他操作 (3)容错性:获得锁的进程若Crash,不影响到后续 ...

最新文章

  1. 一文学会基于R的静态和动态网络绘制
  2. 代码实现UISlider 和 UISwitch
  3. socket编程方法,概念
  4. 3.2.3 OS之页面置换算法(最佳置换算法、先进先出置换算法、最近最久未使用置换算法、普通时钟置换算法、改造型时钟置换算法)
  5. 32汇编语言程序说明_Linux 汇编语言学习--编译和链接
  6. loading linux img2a,嵌入式Linux中initrd的应用--浅析ramdisk、ramfs、initrd和initramfs
  7. 大数据中,机器学习和数据挖掘的联系与区别
  8. 荐书送书丨《PostgreSQL实战》、《PostgreSQL修炼之道:从小工到专家(第2版)》...
  9. Linux 离奇磁盘爆满,如何解决? | 原力计划
  10. mysql表utf-8 字符串入库编码异常
  11. 将SVG 转换为png -- ImageMagick 转换 svg 为透明png 图
  12. 【Android】关于ARouter转场动画的问题
  13. 计算机科学美国大学专业,2015年U.S.NEWS计算机科学专业美国大学排名
  14. 杜凯杰教学数据分析:python 图片爬取 爬取各校校花图片
  15. 亚马逊运营知识:ASIN文案编写技巧
  16. Unity 3D数学 — 向量运算
  17. 手机屏幕常见故障_手机测试常见问题总结!
  18. (错误)SyntaxError: invalid syntax
  19. Java 火车票订票系统
  20. 【C语言—零基础第九课】函数中的爱恨情仇

热门文章

  1. 【Python】matplotlib画图设置标题、轴标签、刻度、刻度标签(系列1)
  2. 如何使用ChatGPT做一份五一出游攻略?
  3. NLP-D38-nlp比赛D7-pytorch踩坑继续刷题
  4. Xtend == 与 ===
  5. linux中的xorg进程占用内存资源释放
  6. npm中node更新_如何在Node中管理NPM和功能时保持理智
  7. rnn 循环神经网络
  8. ChatGPT 大行其道,带你走近 AIGC
  9. 从业务开始:一招攻破数据分析思路大难题
  10. 下载到的电子书格式是Mobi,这种格式能否在MAC电脑上打开?