前言
- 参考:
- 《深入理解Java虚拟机》
遇到的问题
- 互斥量是什么?可以实现总量级锁,操作的时候会存在内核态与用户态的切换。除这些作用之外,互斥量是怎么实现的?
- 轻量级锁和偏向锁的原理,用的多吗?秋招会问么?(一般来说,了解即可,遇到再说。多看面经)
介绍
- 高效并发是从JDK 5升级到JDK 6后一项重要的改进项,HotSpot虚拟机开发团队在这个版本上花费了大量的资源去实现各种锁优化技术,如适应性自旋锁、锁消除、锁膨胀、轻量级锁、偏向锁等,这些技术都是为了在线程之间更加高效地共享数据及解决竞争问题,从而提高程序的执行效率。
自旋锁与自适应自旋
- 引入:
- 互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很多的压力。
- 同时,虚拟机开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。
- 自旋锁:
- 为了让线程等待,我们让线程执行一个忙循环(自旋),看看持有锁的线程是否很快就会释放锁。——自旋锁。
- 自适应自旋锁:
- 引入:
- 自旋也是会耗费性能的,一定要设置一个自旋次数(限度),没有获得锁就应当使用传统的方式去挂起线程。
- 但是无论是默认值还是自己指定一个自旋次数,对整个Java虚拟机中的所有锁来说都是相同的。
- 介绍:
- 自适应自旋锁是对自旋锁的优化。
- 自适应意味着自旋的次数不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。——自适应自旋锁。
- 举例:
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,那么虚拟机就会认为这次自旋也很有可能再次成功获得锁;如果很少成功,则可以省略掉自旋这个步骤。
- 说明:
- 随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越精准。
- 引入:
锁消除
- 定义:
- 指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。——锁消除
- 锁消除主要依据:(建议略过,了解即可)
- 逃逸分析技术。
锁粗化
- 引入:
- 原则上,写代码的时候同步块越小越好,只在共享数据的实际作用域中才进行同步。这样可以使需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁。
- 总结:减少冲突。
- 特殊情况,如一系列连续的操作都是对同一对象反复加锁和解锁,甚至加锁操作使出现在循环体里的,这样会造成不必要的性能损耗。
- 原则上,写代码的时候同步块越小越好,只在共享数据的实际作用域中才进行同步。这样可以使需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁。
- 定义:
- 如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。——锁粗化
轻量级锁
介绍
- 并不是用来代替重量级锁,而是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能损耗。
- 要理解轻量级锁,以及后面的偏向锁,必须要对HotSpot虚拟机对象的内存布局(尤其是对象头部分)有所了解。
HotSpot虚拟机的对象头分为两部分
- 第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁标记位。这一部分官方称为"Mark Word",这部分是实现轻量级锁和偏向锁的关键。
- 一般都是两个bit用于存储锁标志位。
- 另一部分用于存储指向方法区对象类型数据的指针。
- 如果是数组对象,还会有一个额外的部分用来存储数组长度。
- 补充1:
- 详见《深入理解Java虚拟机》第2章。
- 见表1:HotSpot虚拟机对象头锁状态。
- 表1:HotSpot虚拟机对象头锁状态
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
- 我们简单回顾了对象的内存布局后,接下来就可以介绍轻量级锁的工作过程了。
轻量级锁的工作过程
CAS加锁
- 在代码即将进入同步块synchronized的时候,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
- 名词解释:
- 未锁定:01,还没有线程使用过该对象,或者已经解锁且未升级为重量级锁。
- 线程的栈帧:一个方法对应一个栈帧。
- 锁记录存储锁对象目前的Mark Word的拷贝:官方为这份拷贝加了一个Displaced前缀,即Displaced Mark Word。
- 此时线程堆栈与对象头的状态:
- 对象头为Mark Word,且未指向当前线程的堆栈。(未锁定01)
- 名词解释:
- 然后虚拟机使用CAS操作尝试把对象的Mark Word更新为指向锁记录Lock Record的指针。
- 如果更新成功,即代表该线程拥有了这个对象的锁,并且对象Mark Word(非锁记录)的锁标志位将转变为00(01->00),表示此对象处于轻量级锁定状态。
- 此时线程堆栈与对象头的状态:
- 对象头的Mark Word(Mark Word的存储内容)指向当前线程堆栈中的锁记录(即Displaced Mark Word,放着之前Mark Word的拷贝)。
- 当前线程堆栈指向Mark Word?
- 此时线程堆栈与对象头的状态:
- 如果更新失败,意味着至少存在一条线程与当前线程竞争获取该对象的锁。
- 虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧(如果是,说明当前线程以及拥有了这个对象的锁,直接进入同步块继续执行即可;否则就说明这个锁对象已经被其他线程抢占了。)
- 如果出现两条以上的线程争用同一个锁的情况(),那轻量级锁就不再有效,必须要膨胀为重量级锁(锁标志状态 00->10),此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
- 两条以上的线程争用同一个锁:Mark Word更新失败且对象的Mark Word未指向当前线程的栈帧(表示对象已经被其他线程占用)时。
- 如果更新成功,即代表该线程拥有了这个对象的锁,并且对象Mark Word(非锁记录)的锁标志位将转变为00(01->00),表示此对象处于轻量级锁定状态。
CAS解锁
- 如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。
- 假设能够成功替换,那整个同步过程就顺利完成了;
- 如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。
总结
- 轻量级锁能提升程序同步性能的依据是”对于绝大部分的锁,在整个同步周期内都是不存在竞争的“这一经验法则。
- 如果没有竞争,轻量级锁便可以通过CAS操作成功避免使用互斥量的开销;
- 但是确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
偏向锁
- 目的:
- 消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。
- 补充1:如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把之整个同步都消除掉,连CAS操作都不去做了。
- 偏向:
- 偏向第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
- 使用:
- 默认并非开启,需要手动启用?
- 过程:
- 假设当前虚拟机启用了偏向锁(-XX:+UseBiased Locking),那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为01、把偏向模式设置为1,表示进入偏向模式。
- 同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中。
- 如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的相关同步块是,虚拟机都可以不在进行任何同步操作(如加锁、解锁及对Mark Word的更新操作等)。
- 一旦出现另外一个线程去尝试获取这个锁,偏向模式立马宣告结束。
- 根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为0),撤销后标志位恢复到未锁定(01)或轻量级锁(00),后续的同步操作就按照上面介绍的轻量级锁那样去执行(CAS加锁解锁,遇到竞争膨胀为重量级锁)。
- 偏向模式针对的是对象,如果失效,这个对象在这个程序中就永远不会启用了(除非手动,不过没多大用了)。
- 理解:当另一个线程尝试获取该锁时,立马撤销偏向锁。如果偏向锁为未锁定状态,撤销偏向锁之后标志位恢复为未锁定01;否则撤销之后标志位恢复为轻量级锁00。
- 因为另一个线程尝试获取该锁可能成功,那么就会直接变成轻量级锁,否则变为未锁定。
- 根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为0),撤销后标志位恢复到未锁定(01)或轻量级锁(00),后续的同步操作就按照上面介绍的轻量级锁那样去执行(CAS加锁解锁,遇到竞争膨胀为重量级锁)。
总结——自我感觉总结的很好,可对付秋招
- 自旋锁:很多应用中共享数据的锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复线程并不值得,可以自旋固定次数等待。
- 自适应自旋锁:相比于自旋锁,自旋次数不是固定的,而是有前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 锁消除:运行时,对检测到不可能存在共享数据竞争的锁进行消除。
- 锁粗化:原则上同步块越小越好,造成冲突小,但是如果一系列连续操作都是对同一对象反复加锁和解锁,则可将加锁同步的范围扩展到整个操作序列的外部。
- 重量级锁:使用互斥量,涉及到内核态和用户态的转换,性能较低,JDK1.6之前主要使用。
- 轻量级锁:在发生竞争之前都使用CAS操作替代互斥量,发生竞争之后膨胀为重量级锁。
- 偏向锁:未发现有多个线程使用同一共享数据之前使用CAS操作消除掉同步块原语,发现有其他线程使用该共享数据之后立马撤销偏向模式,后续同步操作全安轻量级锁去执行。
- 注意:当另一个线程尝试获取该锁时,立马撤销偏向锁。如果偏向锁为未锁定状态,撤销偏向锁之后标志位恢复为未锁定01;否则(另一个线程获取锁成功)撤销之后标志位恢复为轻量级锁00。
- 补充:
- 轻量级锁和偏向锁的底层实现涉及到Java对象头中的Mark Word(Jaca虚拟机的内容),具体秋招应该不会怎么问,问到再去好好复习一下?不问但是一定要知道这么个东西。
- 轻量级锁和偏向锁是这里最难的知识点了,又特别是轻量级锁,记忆上面偏向锁稍微多一点。