Java并发-synchronized


  • synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响
  • Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。
  • Java 参考了 MESA 模型(管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列),语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量
  • Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于 ObjectMonitor 实现
    • 在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略(QMode=0)是:如果EntryList为空,则将 cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取 锁。EntryList不为空,直接从EntryList中唤醒线程
    • 锁状态被记录在每个对象的对象头的Mark Word中
ObjectMonitor() {
_header = NULL; //对象头 markOop
_recursions = 0; // 锁的重入次数
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是竞争锁失败的线程)



对象的内存布局

Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域

  • 对象头(Header):比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)
  • 实例数据 (Instance Data):存放类的属性数据信息,包括父类的属性信息
  • 对齐填充(Padding):对象起始地址必须是8字节的整数倍

对象头

HotSpot虚拟机的对象头包括

  • Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit
  • Klass Pointer:对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节(-XX:-UseCompressedOops 关闭指针压缩)
  • 数组长度(只有数组对象有):如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节

使用JOL工具查看内存布局

  • OFFSET:偏移地址,单位字节
  • SIZE:占用的内存大小,单位为字节
  • TYPE DESCRIPTION:类型描述,其中object header为对象头
  • VALUE:对应内存中当前存储的值,二进制32位
<dependency>
	<groupId>org.openjdk.jol</groupId> 
	<artifactId>jol‐core</artifactId>
</dependency>
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

Mark Word 记录锁状态(markOop.hpp)

32位
64位

  • hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里
  • age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代
  • biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位
  • lock: 锁状态标识位。区分锁状态,但是11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效
  • JavaThread*: 保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念
  • epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针

enum { 
	locked_value = 0, //00 轻量级锁
	unlocked_value = 1, //001 无锁
	monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁 
	marked_value = 3, //11 GC标记
	biased_lock_pattern = 5 //101 偏向锁
};

  • 偏向锁:偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多 线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果
    • 匿名偏向状态:新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)
    • 偏向锁延迟偏向:HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁
    • 偏向锁撤销
      • 调用对象HashCode:调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的
        • 轻量级锁会在锁记录中记录 hashCode
        • 重量级锁会在 Monitor 中记录 hashCode
        • 当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向
          • 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁
          • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁
      • 调用wait/notify
        • 偏向锁状态执行obj.notify() 会升级为轻量级锁
        • 调用obj.wait(timeout) 会升级为重量级锁
    • 偏向锁批量重偏向&批量撤销:当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制
      • 批量重偏向:默认class偏向撤销阈值20
        • 以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向
      • 批量撤销:默认class偏向撤销阈值40
        • 每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id
        • 当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑
      • 应用场景
        • 批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作
        • 批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的
      • JVM参数
        • -XX:BiasedLockingBulkRebiasThreshold:偏向锁批量重偏向阈值
        • -XX:BiasedLockingBulkRevokeThreshold:偏向锁批量撤销阈值
      • 批量重偏向和批量撤销是针对类的优化,和对象无关
      • 偏向锁重偏向一次之后不可再次重偏向
      • 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
  • 轻量级锁:倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁
    • 轻量级锁所适应的场景是:线程交替执行同步块
    • 偏向锁升级轻量级锁
    • CAS自旋修改Mark Word中锁记录的地址,失败超过一定次数轻量级锁会膨胀为重量级锁
  • 重量级锁
    • 自旋优化:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
      • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
      • 自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
      • 不能控制是否开启自旋功能
      • 自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
  • 锁粗化:假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部
  • 锁消除:即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间
    • ‐XX:+EliminateLocks 开启锁消除


  • 逃逸分析(Escape Analysis):是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
    • 逃逸分析的基本行为就是分析对象动态作用域
    • 方法逃逸:对象逃出当前方法
    • 线程逃逸:对象逃出当前线程
    • 使用逃逸分析,编译器可以对代码做如下优化
      • 同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
      • 将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
      • 分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
    • JVM参数
      • ‐XX:+DoEscapeAnalysis 开启逃逸分析
      • ‐XX:+EliminateAllocations 开启标量替换

文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录