我们都知道 synchronized 关键字能实现线程安全,但是你知道这背后的原理是什么吗?今天我们就来讲一讲 synchronized 实现线程同步背后的原因,以及相关的锁优化策略吧。

synchronized 背后的原理

synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码只需要一个指明一个要锁定或解锁的对象。如果 Java 程序中指明了对象参数,那么就用这个对象作为锁。

如果没有指定,那么就根据 synchronized 修饰的是实例方法还是类方法,去拿对应的对象实例或 Class 对象来作为锁对象。因此我们可以知道,synchronized 关键字实现线程同步的背后,其实是 Java 虚拟机规范对于 monitorenter 和 monitorexit 的定义。

在 Java 虚拟机规范对 monitorenter 和 monitorexit 的行为描述中,有两点需要特别注意。

  1. synchronized 同步块对同一条线程是可重入的,也就是不会出现自己把自己锁死的问题。
  2. 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

synchronized 关键字在 JDK1.6 版本之前,是通过操作系统的 Mutex Lock 来实现同步的。而操作系统的 Mutex Lock 是操作系统级别的方法,需要切换到内核态来执行。这就需要从用户态转换到内核态中,因此我们说 synchronized 同步是重量级的操作。

synchronized 锁优化

在 JDK1.6 版本中,HotSpot 虚拟机开发团队花了很大的精力去实现各种锁优化技术,如:适应性自旋、锁消除、锁粗话、偏向锁、轻量级锁等。其中最重要的是:自旋锁、轻量级锁、偏向锁这三个,我们重点讲这三个锁优化。

自旋锁与自适应自旋

对于重量级的同步操作来说,最大的消耗其实是内核态与用户态的切换。但很多时候,对于共享数据的操作时间可能很短,比内核态切换到用户态这个耗时还短。

于是有人就想:如果有多个线程并发去获取锁的时候,如果能让后面那个请求锁的线程「稍等一下」,不放弃 CPU 的执行时间,看看持有锁的线程是否会很快释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 从理论上来看,如果所有线程都很快地获取锁、释放锁,那么自旋锁是可以带来较大的性能提升的。自旋锁在 JDK 1.4.2 中就已经引入,默认自旋 10 次。但自旋锁默认是关闭的,在 JDK 1.6 中才改为默认开启了。

自旋等待虽然避免了线程切换的开销,但还是要占用处理器的时间。如果锁被占用的时间段,自旋等待的效果就会非常好。但如果锁被长时间占用,那么自旋的线程就会白白消耗处理器的资源,从而带来性能上的浪费。

为了解决特殊情况下自旋锁的性能消耗问题,在 JDK1.6 的时候引入了自适应的自旋锁。 自适应意味着自旋时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者状态决定。如果在同一锁对象上,自旋等待刚刚成功获得过锁,那么虚拟机认为这次自旋也很有可能再次成功,进而允许线程自旋更长时间,例如自旋 100 个循环。

但如果对于某个锁,自旋很少成功获得过。那虚拟机为了避免浪费 CPU 资源,有可能省略掉自旋过程。有了自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对锁的状态预测就越准,虚拟机也会变得越来越聪明。

轻量级锁

轻量级锁是 JDK1.6 加入的新型锁机制,名字中的「轻量级」是相对于操作系统互斥量这个重量级锁而言的。轻量级锁诞生的原因,是由于对于绝大部分的锁而言,整个同步周期都不存在竞争。如果没有竞争的话,那就没必要使用重量级锁了,于是就诞生了轻量级锁来提高效率。

对于轻量级锁来说,其同步的流程如下:

  1. 在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 01 状态),那么虚拟机会在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 拷贝。
  2. 虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果更新动作成功了,那么线程就泳衣了该对象的锁,并且对象 Mark Word 的锁标志位就变成了 00,表示此对象处于轻量级锁定状态。

简单地说,轻量级锁的同步流程可以总结为:使用 CAS 操作,在线程栈帧与锁对象建立双向的指针。

在没有线程竞争的情况下,轻量级锁使用 CAS 自旋操作避免了使用互斥量的开销,提高了效率。但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作。因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

偏向锁

偏向锁是 JDK1.6 中引入的一项优化,它的意思是这个锁会偏向于第一个获得它的线程。如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。 对于偏向锁来说,其同步流程如下所示:

  1. 假设当前虚拟机启动了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机将会把对象的锁标志位设置为 01,偏向锁位设置为 1。同时使用 CAS 操作将线程 ID 记录在对象的 MarkWord 之中。如果 CAS 操作成功,那么持有偏向锁的线程进入锁对应的同步块时,虚拟机将不再进行任何同步操作。
  2. 当有另外一个线程尝试去获取这个锁时,根据锁对象目前是否处于锁定状态,将其恢复到未锁定(01)或轻量级锁定(00)状态。随后的同步操作,就向上面介绍的轻量级锁那样执行。

可以看到偏向锁还是需要做一些 CAS 操作,但是对比起轻量级锁来说,其要设置的内容大大减少了,因此也提高了一些效率。偏向锁可以提高带有同步但无竞争的程序性能。 它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。

优化后的锁获取流程

经过 JDK1.6 的优化,synchronized 同步机制的流程变成了:

  1. 首先,synchronized 会尝试使用偏向锁的方式去竞争锁资源,如果能够竞争到偏向锁,表示加锁成功直接返回。
  2. 如果竞争锁失败,说明当前锁已经偏向了其他线程。需要将锁升级到轻量级锁,在轻量级锁状态下,竞争锁的线程根据自适应自旋次数去尝试抢占锁资源。
  3. 如果在轻量级锁状态下还是没有竞争到锁,就只能升级到重量级锁。在重量级锁状态下,没有竞争到锁的线程就会被阻塞。处于锁等待状态的线程需要等待获得锁的线程来触发唤醒。

上面的锁获取流程,可以用如下的示意图来表示:

总结

本文首先简单讲解了 synchronized 关键字实现同步的原理,其实是通过 Java 虚拟机规范对于 monitorenter 和 monitorexit 的支持,从而使得 synchronized 能够实现同步。而 synchronized 同步本质上是通过操作系统的 mutex 锁来实现的。由于操作操作系统 mutex 锁太过于消耗资源,因此在 JDK1.6 后 HotSpot 虚拟机做了一系列的锁优化,其中最重要的便是:自旋锁、轻量级锁、偏向锁。这三个锁的诞生原因,以及提升的点如下表所示。

现状 锁名称 收益 使用场景
大多数情况下,等待锁的时间比操作系统 mutex 短得多 自旋锁 减少内核态与用户态切换的开销 线程获取锁时间较短的情况
大多数情况下,锁同步期间没有线程竞争 轻量级锁 与自旋锁相比,减少了自旋时间 没有线程竞争锁
大多数情况下,锁同步期间没有线程竞争 偏向锁 与轻量级锁相比,减少了多余的对象复制操作 没有线程竞争锁

从上面表格可以看到,自旋锁、轻量级锁、偏向锁,他们的优化是逐渐深入的。

  1. 对于重量级锁来说,自旋锁减少了互斥量的内核、用户态切换开销。
  2. 轻量级锁,是自旋锁再 Java 内存模型里的直接应用,其同样是减少了内核态与用户态的切换开销。
  3. 偏向锁,相对于轻量级锁来说,减少了多余的对象复制操作,因此效率更高一些。

参考资料

最新文章

  1. KMS安装后激活机器
  2. Android webview通过http get下载文件下载两次的问题及解决方法
  3. golang channel basic
  4. TCP/IP详解 学习七
  5. 丢失Ref Edit Control的解决方法
  6. COM ,Threading Models,apartments,RPC
  7. mongodb 查询使用
  8. UVa 1635 (唯一分解定理) Irrelevant Elements
  9. 一滴一点vim(学习+备忘)
  10. 图的遍历(bfs 和dfs)
  11. BIT_COUNT()和BIT_OR()
  12. Weinre - 远程调试工具
  13. 【翻译】MVC Music Store 教程-概述(三)
  14. JNI(2)
  15. 【Android工具类】Activity管理工具类AppManager
  16. Android - 传统蓝牙(蓝牙2.0)
  17. codeforces570C
  18. .net导出Excel几种方式比较
  19. ToolBar样式颜色,图标设置
  20. 648. Replace Words 替换成为原来的单词

热门文章

  1. 利用java反射机制实现List>转化为List
  2. 【第三课】常用的Linux命令(学习笔记)
  3. vscode 开发项目, Prettier ESLint的配置全攻略(基础篇)
  4. FreeRTOS --(10)任务管理之任务延时
  5. vue 收集表单数据 (有错误的请各位大佬指点)
  6. Linux-centos8实现私有CA和证书申请
  7. 2021春季学期华清大学EE数算OJ3:岩石的重量
  8. Apache ShenYu:分析、实现一个 Node.js 语言的 HTTP 服务注册客户端(HTTP Registry)
  9. docker 安装和错误解决方案
  10. 设置VisualStudio以管理员身份运行