synchronized的优化

SoruxGPT
发布于 2024-08-23 / 5 阅读
0

synchronized的优化

synchronized的优化

Java对象头

Java对象头是HotSpot虚拟机中每个对象在内存中都有的元数据结构。对象头的组成主要分为以下几个部分:

组成部分

位数(32位JVM)

位数(64位JVM)

描述

Mark Word

32位

64位

用于存储对象的运行时数据,如哈希码、GC状态标志、锁信息等。

Klass Pointer

32位

64位(压缩指针为32位)

指向对象的类元数据的指针,表示这个对象是哪个类的实例。

对齐填充(Padding)

可选

可选

为了保证对象大小是8字节的倍数,可能会添加填充字节。

MarkWord的结构

32位:

64位:

Monitor的结构

Monitor 被翻译为监视器管程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

  • 刚开始 Monitor 中 Owner 为 null,说明没有人抢锁

  • 当 Thread-1 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-1,Monitor中只能有一个 Owner

  • 在 Thread-1 上锁的过程中,如果 Thread-2,Thread-3,Thread-4 也来执行 synchronized(obj),就会进入EntryList BLOCKED

  • Thread-1 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

  • 图中 WaitSet 中的 Thread-5,Thread-6 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,需要通过notify、notifyAll来唤醒。

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果

  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

代码同步块是通过使用monitorentermonitoreexit指令实现的,当线程执行到monitorenter指令的时候,将会尝试获取对象所对应的monitor的使用权,也就是获取到对象的锁。

锁的升级与对比

在Java6中为了减少获取锁和释放锁带来的性能损耗,引入了“偏向锁”和“轻量锁”。

无锁状态、偏向锁、轻量级锁、重量级锁。

锁可以升级,但是不能降级。

轻量级锁

场景:有一个方法或对象虽然是多线程的,但是岔开时间使用的。

  • 创建锁记录(Lock Record),每一个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

  • 让锁记录中的Object reference指向锁对象,并且尝试用cas替换Object的mark word,把mark word的内容写入锁中

  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

  • 如果cas失败了,有两种情况

    • 如果是其他线程线程想要来抢锁,就会进入锁膨胀的过程,变为重量级的锁,会为锁对象关联一个monitor对象,争夺monitor的所属权

    • 如果是自己这个线程的锁重入,那就再加一条Lock Record,重入计数+1。

  • 当退出synchronized代码块的时候,如果有取值为null的锁记录,表示有重入,重制锁记录,重入计数减1。

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

    • 成功,则解锁成功

    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

轻量级锁膨胀为重量级锁

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

  • 这时 Thread-1 加轻量级锁失败,Thread -1尝试自旋转获取锁,如果没有获取到,进入锁膨胀状态,升级为重量级锁。

  • 然后把自己加入到monitor中的EntryList中进行等待获取锁。

    • Thread -0结束后,会尝试Cas替换Object中的Mark Word,这个时候会发现替换失败,Thread -0就会释放锁,并且通知所有线程可以来抢锁了。

(竞争重量级锁时的)自旋优化

自旋成功:

自旋失败:

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • Java 7 之后不能控制是否开启自旋功能

偏向锁

在大多数情况下,锁不仅不存在多线程之间的竞争,而且还总是由同一个线程多次获取到锁,所以,为了让线程获取锁的代价更低就引入了偏向锁。当一个线程访问同步块并获取到锁的时候,会在对象头和栈帧中的锁记录里存放锁偏向的线程ID,以后这个线程在进入、退出同步块的时候就不需要进行CAS操作来加锁和解锁吗,只需要看对象头中的Mark Word中是否存储这指向当前线程的偏向锁。

static final Object obj = new Object();
​
public static void m1() {
    synchronized( obj ) {
        // 同步块 A
        m2();
    }
}
​
public static void m2() {
    synchronized( obj ) {
        // 同步块 B
        m3();
    }
}
​
public static void m3() {
    synchronized( obj ) {
        // 同步块 C
    }
}

调用HashCode之后就不会再进入偏向锁状态,因为没地方存HashCode。

总结

如果偏向锁是开启的,并且没有延迟,有延迟的不会刚开始就创建偏向锁

刚开始从无锁状态--->偏向锁的状态

然后如果有有人与偏向锁的持有者进行竞争就会升级会轻量锁

然后从偏向锁状态--->轻量锁

如果继续有人和轻量锁的持有者竞争,一开始会尝试自旋获取锁,因为重量级锁对性能的损耗还是比较大。

如果自旋过程中获取到了锁,就不会升级为重量级的锁

如果自选过程中没有获取到锁,就会升级会重量级别的锁