第十章详解synchronized锁升级

news2024/12/24 2:26:57

文章目录

  • 升级的流程
    • 为什么要引入锁升级这套流程
    • 多线程访问情况
    • 具体流程
  • 轻量级锁
    • 如何使用CAS实现轻量级锁
      • CAS加锁成功
      • CAS加锁失败
      • CAS进行解锁
    • 总结
      • 何时变为重量级锁
  • 锁膨胀
    • 自旋优化
  • 偏向锁
    • 主要作用
    • 偏向状态
      • 测试
      • 撤销偏向锁
    • 撤销 - 调用对象 hashCode
    • 撤销 - 其它线程使用对象
    • 撤销 - 调用 wait/notify
    • 批量重偏向
    • 批量撤销
  • 小总结-面试中的高频考点
    • 锁升级发生后,hashcode去哪了
    • 锁消除
    • 锁粗化

升级的流程

为什么要引入锁升级这套流程

image-20230620232505153

  • Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入(我们的Monitor对象对应有一个阻塞队列_EntryList),需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
    • 陷入指令是在用户态执行的,执行陷入指令之后会立即引发一个内中断,从而CPU进入核心态,从用户态转为内核态通过陷入指令
  • 在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的synchronized效率低的原因Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

多线程访问情况

  • 只有一个线程来访问,有且唯一Only One
  • 有多个线程(2个线程A、B来交替访问)
    • 不是同时去竞争锁,例如A访问完,A释放了锁,B才来访问,是交错开的
  • 竞争激烈,更多个线程来访问
    • 竞争激烈,比如A占用锁,B也想占用锁,造成竞争

具体流程

  • synchronized用的锁是存在Java对象头里的Mark Word中,锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位

image-20230620215312621

  • 偏向锁:MarkWord存储的是偏向的线程ID
  • 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针
  • 重量锁:MarkWord存储的是指向堆中的monitor对象的指针

img

  • 将锁信息放入对象头中
  • 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  • 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  • 实现轻量级锁的时候大概率用到的自旋锁策略
  • 是一种不公平锁
  • 是一种可重入锁
  • 不是读写锁

引入对象头后synchronized的重量级锁流程

在这里插入图片描述

轻量级锁

  • 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
    • 轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻寨

主要作用

  • 有线程来参与锁的竞争,但是获取锁的冲突时间极短
  • 本质就是自选锁CAS

假设有两个方法同步块,利用同一个对象加锁

public class SynchronizedDemo1 {
    static final Object obj = new Object();
    public static void method1() {
        synchronized( obj ) {
            // 同步块 A
            method2();
        }
    }
    public static void method2() {
        synchronized( obj ) {
            // 同步块 B
        }
    }
}

image-20230620235307246

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

如何使用CAS实现轻量级锁

CAS加锁成功

image-20230620235328482

  • 为什么我们的lock record地址后面带有 两个0
    • 我们可以在上图中看到我们的轻量级锁的状态是00
    • 我们的CAS(比较和交互),进行判断的就是这个状态位
      • v A B 对应的就是 v是内存中Object中的值对象的锁状态位 A是01 B是00
      • 当我们Object中的状态位是01时,要么是无锁要么是偏向锁,可以进行加轻量级锁
  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁

CAS加锁失败

  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

image-20230620235347387

  • 为什么CAS失败,因为我们的期望值是01,但是内存中的Obeject对象的锁标志位是00,所以 CAS操作失败
  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

对于替换失败的处理,就是进行自旋操作,也就是循环执行CAS操作,所以也称自旋锁,当我们的自旋次数到达了10次,就进入了对应的锁膨胀,也就是变为重量级锁

CAS进行解锁

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

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

总结

轻量级锁的获取

  • 轻量级锁是为了在线程近乎交替执行同步块时提高性能。

  • 主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗.说白了先自旋,不行才升级阻寨。

  • 升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁

轻量级锁的加锁

  • JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方成为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

  • 自旋CAS:不断尝试去获取锁,能不升级就不往上捅,尽量不要阻寨

轻量级锁的释放

  • 在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻寨的线程。

何时变为重量级锁

自旋达到一定次数和程度
java6之前

  • 默认启用,当自旋次数为十次时
  • 或者自旋次数超过核数一半

java6之后

  • 线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。

反之

  • 如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。

    • 总之,自适应意味着自选的次数不是固定不变的,而是根据:同一个锁上一次自旋的时间和拥有锁线程的状态来决定。

轻量锁和偏向锁的区别和不同

  • 争夺轻量级锁失败时,自旋尝试抢占锁
  • 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁

锁膨胀

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

static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块
	}
}

image-20230621125752248

这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

  • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 10表示锁状态为重量级锁
    • Monitor并不是变成重量级锁才出现的,而是对象生成的时候就有了
  • 然后自己进入 Monitor 的 EntryList BLOCKED

当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

image-20230620235754768

  • 当变为重量级锁,对应的hashcode值放在Monitor对象中

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况

线程 1 (core 1 上)对象 Mark线程 2 (core 2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行完毕10(重量锁)重量锁指针自旋重试
成功(解锁)01(无锁)自旋重试
-10(重量锁)重量锁指针成功(加锁)
-10(重量锁)重量锁指针执行同步块
-

自旋重试失败的情况

线程 1(core 1 上)对象 Mark线程 2(core 2 上)
-10(重量锁)-
访问同步块,获取 monitor10(重量锁)重量锁指针-
成功(加锁)10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针-
执行同步块10(重量锁)重量锁指针访问同步块,获取 monitor
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针自旋重试
执行同步块10(重量锁)重量锁指针阻塞
-
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

偏向锁

当线程A第一次竞争到锁时,通过修改Mark Word中的偏向ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。

主要作用

当一段同步代码一直被同一个线程多次访问,由于只有一个线程访问那么该线程在后续访问时便会自动获得锁。

  • 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
  • Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
public class SynchronizedDemo2 {
    static final Object obj = new Object();

    public static void main(String[] args) {
        m1();
    }
    public static void m1() {
        synchronized( obj ) {
            // 同步块 A
            m2();
        }
    }
    public static void m2() {
        synchronized( obj ) {
            // 同步块 B
            m3();
        }
    }
    
    public static void m3() {
        synchronized( obj ) {
        }
    }
}

image-20230621132950459

  • 如果是轻量级锁,就算是同一个线程,也会生成对应的Lock Record,和对应的对象的Mard Word进行CAS操作,只是CAS不成功
    • CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用重量级锁(Monitor->Mutex Lock), 这里的排他时间要短的多
  • 只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

偏向状态

|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

测试

测试延迟特性和偏向锁

 // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
    public static void main(String[] args) throws IOException {
        Dog d = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(d);
        new Thread(() -> {
            log.debug("synchronized 前");
            System.out.println(classLayout.toPrintableSimple(true));
            synchronized (d) {
                log.debug("synchronized 中");
                System.out.println(classLayout.toPrintableSimple(true));
            }
            log.debug("synchronized 后");
            System.out.println(classLayout.toPrintableSimple(true));
        }, "t1").start();
    }
11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
  • 锁状态101说明初始就是偏向锁。证明了偏向锁是默认开启的
  • 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

测试禁用

  • 在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
11:13:10.018 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
11:13:10.021 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000
11:13:10.021 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  • 锁状态001说明初始是无锁,证明了偏向锁禁止开启了
  • 当进入synchronized代码中,锁状态00,说明直接变成轻量级锁
  • 解锁后,也直接变成了无锁状态

测试 hashCode

  • 正常状态对象一开始是没有 hashCode 的,第一次调用才生成

撤销偏向锁

  • 当有另外线程逐步来竞争所的时候,就不能再使用偏向锁了,要升级为轻量级锁
    • 偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。
  • 竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁
    • 撇销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

Java15逐步废弃偏向锁

但是过去看到的性能提升,在现在看来已经不那么明显了。受益于偏向锁的应用程序,往往是使用了早期 Java 集合 API的程序(JDK 1.1),这些 API(Hasttable 和 Vector) 每次访问时都进行同步。JDK 1.2 引入了针对单线程场景的非同步集合(HashMap 和 ArrayList),JDK 1.5 针对多线程场景推出了性能更高的并发数据结构。这意味着如果代码更新为使用较新的类,由于不必要同步而受益于偏向锁的应用程序,可能会看到很大的性能提高。此外,围绕线程池队列和工作线程构建的应用程序,性能通常在禁用偏向锁的情况下变得更好。

偏向锁为同步系统引入了许多复杂的代码,并且对 HotSpot 的其他组件产生了影响。这种复杂性已经成为理解代码的障碍,也阻碍了对同步系统进行重构。因此,我们希望禁用、废弃并最终删除偏向锁。

撤销 - 调用对象 hashCode

  • 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
    • 轻量级锁会在锁记录中记录 hashCode
    • 重量级锁会在 Monitor 中记录 hashCode
  • 在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
   public static void main(String[] args) throws IOException {
        Dog d = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(d);
        log.debug(d.hashcode());
        new Thread(() -> {
            log.debug("synchronized 前");
            System.out.println(classLayout.toPrintableSimple(true));
            synchronized (d) {
                log.debug("synchronized 中");
                System.out.println(classLayout.toPrintableSimple(true));
            }
            log.debug("synchronized 后");
            System.out.println(classLayout.toPrintableSimple(true));
        }, "t1").start();
    }
11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
11:22:10.393 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000
11:22:10.393 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
  • 我们发现我们的锁状态是从001(无锁)——>00(轻量级锁)
  • 说明调用了hashCode方法,撤销了偏向锁

撤销 - 其它线程使用对象

  • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
    private static void test2() throws InterruptedException {
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
             }
            synchronized (TestBiased.class) {
                TestBiased.class.notify();
             }
			// 如果不用 wait/notify 使用 join 必须打开下面的注释
			// 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
			/*try {
				System.in.read();
			} catch (IOException e) {
				e.printStackTrace();
			}*/
        }, "t1");
        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (TestBiased.class) {
                try {
                    TestBiased.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }, "t2");
        t2.start();
    }
[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
  • 我们这里利用wait/notify来控制线程同步(这里锁的是TestBiased.class,而不是Dog对象),是为了让t1,t2线程错开争取锁,而不是同时争取锁,因为同时争取锁会导致变成重量级锁
  • t1先得到Dog对象,所以是锁状态101,所以是偏向锁,且存了t1线程的id
  • t2去获取锁的时候,偏向锁升级,变为轻量级锁 101->00
  • 当t2解锁后,变成无锁 00->001

撤销 - 调用 wait/notify

 public static void main(String[] args) throws InterruptedException {
        Dog d = new Dog();
        Thread t1 = new Thread(() -> {
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
                try {
                    d.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }, "t1");
        t1.start();
        new Thread(() -> {
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (d) {
                log.debug("notify");
                d.notify();
            }
        }, "t2").start();
    }
[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101
[t2] - notify
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
  • 我们这里用wait/notify。这里用的对象是Dog对象
  • 我们这里使用了wait/notify 从101(轻量级锁)->10(重量级锁)
  • 因为我们的wait和notify底层需要Monitor进行实现

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID

  • 当撤销偏向锁阈值超过 20 次后,JVM 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
   private static void test3() throws InterruptedException {
        Vector<Dog> list = new Vector<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }
            synchronized (list) {
                list.notify();
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (list) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("===============> ");
            for (int i = 0; i < 30; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }, "t2");
        t2.start();
    }
  • 对list的对象进行wait/notify,是为了不让t1,t2线程不会同时产生竞争锁
[t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
....
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - ===============>
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
....
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
  • 我们发现当只有一个线程t1进行加锁,所以加的都是偏向锁(101)
  • 当t1线程执行完,当t2线程线程在给对应的对象进行加锁,我们看到在前19个对象,因为由于偏向锁,所以从偏向锁变成了轻量级锁 101->00(锁升级)
  • 但是从第20次,我们不是将偏向锁变成轻量级锁,而是重偏向,从偏向t1变成偏向t2

批量撤销

当撤销偏向锁阈值超过 40 次后,JVM会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

	 static Thread t1,t2,t3;
    private static void test4() throws InterruptedException {
        Vector<Dog> list = new Vector<>();
        int loopNumber = 39;
        t1 = new Thread(() -> {
            for (int i = 0; i < loopNumber; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }
            LockSupport.unpark(t2);
        }, "t1");
        t1.start();
        t2 = new Thread(() -> {
            LockSupport.park();
            log.debug("===============> ");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                synchronized (d) {
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            LockSupport.unpark(t3);
        }, "t2");
        t2.start();
    	 t3 = new Thread(() -> {
         LockSupport.park();
         log.debug("===============> ");
         for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
         }
    	 }, "t3");
		t3.start();
		t3.join();
		log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}

小总结-面试中的高频考点

锁升级发生后,hashcode去哪了

锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,己经没有位置再保存哈希码,GC年龄了,那么这些信息被移动到哪里去了呢?

  • 在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。
  • 对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法己经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的话,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。
    • 故当一个对象正处于偏向锁状态,又收到需要计算一致性哈希码请求时,它的偏向状态会立即撤销,并且锁会膨胀为重量级锁
  • 升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
  • 升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。

锁消除

  • 这个锁对象并没有被共用扩散到其它线程使用,
  • 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
    static int x = 0;
    @Benchmark
    public void a() throws Exception {
       x++;
     }
    @Benchmark
    public void b() throws Exception {
       Object o = new Object();
          synchronized (o) {
          x++;
       }
    }
}

java -jar benchmarks.jar

Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op
c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op

java -XX:-EliminateLocks -jar benchmarks.jar——禁止锁消除

Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op
c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op

锁粗化

  • 对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化

假如方法中首位相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请使用即可,避免次次都申请和释放锁,提升了性能

public class SynchronizedDemo3 {
    static Object objectLock = new Object();
    public static void main(String[] args) {
        new Thread(()->{
            synchronized (objectLock){
                System.out.println("11111111");
            }
            synchronized (objectLock){
                System.out.println("22222222");
            }
            synchronized (objectLock){
                System.out.println("33333333");
            }
            synchronized (objectLock){
                System.out.println("44444444");
            }
            synchronized (objectLock){
                System.out.println("11111111");
                System.out.println("22222222");
                System.out.println("33333333");
                System.out.println("44444444");
            }
        },"t1").start();

    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/670732.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

js:codemirror实现在线代码编辑器代码高亮显示

CodeMirror is a versatile text editor implemented in JavaScript for the browser. It is specialized for editing code, and comes with a number of language modes and addons that implement more advanced editing functionality. 译文&#xff1a;CodeMirror是一个多…

第二章:软件工程师必备的网络基础

目录 一、网线的制作 二、集线器、交换机介绍 三、路由器的配置 一、网线的制作 1.1、水晶头 ​​​ 1.2、网线钳 1.3、网线的标准 T568A标准&#xff08;交叉线&#xff09;&#xff1a; 适用链接场合&#xff1a;电脑-电脑、交换机-交换机、集线器-集线器 接线顺序&…

【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html# 第三…

有源电力滤波器及配电能效平台在污水处理厂中的应用

【摘要】为减少污水处理设备产生的各次谐波&#xff0c;通过确定主要谐波源&#xff0c;检测和计算谐波分量&#xff0c;采用有源电力滤波器进行谐波治理&#xff0c;大幅降低了电力系统中的三相电流畸变率&#xff0c;提高了电能质量&#xff1b;抑制了谐波分量&#xff0c;减…

doris docker部署和本地化部署 1.2.4.1版本

写在前面 以下操作语句按顺序执行即可&#xff0c;注意切换目录的命令一定记得执行&#xff0c;如果需要改动的地方会有${}注释&#xff0c;其余不需要任何改动&#xff0c;默认安装版本为1.12.4&#xff08;稳定版&#xff09; 本地化部署 下载 # 创建目录 mkdir /data/sof…

软件测试日常工作和前景是怎么样的?

笔者从测试的工作情况&#xff0c;职业发展&#xff0c;还有测试的工作日常等等来给大家讲解一下软件测试到底是什么样的工作&#xff1f; 通俗来说软件测试工程师就相当于一个质检员&#xff0c;专门处理软件测试质量的工作&#xff0c;不管是功能测试也好&#xff0c;性能测…

BK7231N开发平台原厂烧录工具使用说明

BK7231N开发平台原厂烧录工具使用说明 烧录流程介绍 1.打开原厂烧录工具 以管理员身份打开名为 bk_writer_gui_V1.6.3.exe 的可执行文件。 2. 烧录对象 烧录对象选择 BK7231n 3.烧录地址 当我们烧录UA文件的时候&#xff0c;需要把起始地址设置为&#xff1a; 0X00011000。…

Windows提示“找不到rgss202j.dll”怎么办?

Rgss202j.dll文件是Windows操作系统最重要的系统文件之一&#xff0c;它包含了一组程序和驱动函数。如果此文件丢失或损坏&#xff0c;驱动程序将无法正常工作&#xff0c;并且相应的应用程序也将无法正常启动且运行。通常情况下&#xff0c;造成Rgss202j.dll文件无法找到的原因…

爬虫 - ProtoBuf 协议

一、抓取请求 以下是请求的大致内容&#xff1a; 是乱码&#xff0c;需要解析。 二、解析 通过分析 request 和 response 的 Content-Type: application/x-protobuf 得知&#xff1a;使用了谷歌的 protobuf 协议来传输数据&#xff0c;需要破解。 大致破解过程&#xff…

随时随地保持连接:数字游民适用的远程桌面

随着世界迅速适应数字革命&#xff0c;一种全新的职业——数字游民应运而生。数字游民指利用技术远程办公的专业人群&#xff0c;这是一种允许人们在旅行中办公、不受地点限制的工作生活方式。游牧式工作生活趋势并非一时的风尚&#xff0c;而是我们工作观念的彻底转变&#xf…

MUR8060PT-ASEMI快恢复二极管MUR8060PT

编辑-Z MUR8060PT在TO-247封装里采用的2个芯片&#xff0c;其尺寸都是140MIL&#xff0c;是一款高耐压大电流快恢复二极管。MUR8060PT的浪涌电流Ifsm为600A&#xff0c;漏电流(Ir)为10uA&#xff0c;其工作时耐温度范围为-55~150摄氏度。MUR8060PT采用抗冲击硅芯片材质&#x…

实在智能率先拥抱大模型技术,旗下“AI+RPA”系列产品迎来全面智能升级

实在智能RPA ​ AI时代&#xff0c;所有产品都将迎来用大模型进行全面智能升级。 随着以ChatGPT等为代表的生成式AI持续火热&#xff0c;大型语言模型&#xff08;Large Language Model, LLM&#xff09;领域的研发和布局在国内外有目共睹&#xff0c;微软、谷歌、百度系等生…

回收小程序是什么?有什么特点?

回收小程序旨在为用户提供便捷、环保的废品回收服务。以下是关于上门回收小程序的介绍&#xff1a;回收小程序旨在解决废品回收的难题&#xff0c;为用户提供一种方便、可持续的回收方式。通过小程序&#xff0c;可以轻松预约回收服务&#xff0c;将废品交由专业回收人员处理&a…

基于QFT的量子加法器的原理与实现-mindspore quantum

1 量子Fourier变换 离散Fourier变换以一一个复向量 x 0 , . . . , x N − 1 {x_0},...,{x_{N - 1}} x0​,...,xN−1​为输入&#xff0c;输出的数据是如下复向量 y 0 , . . . , y N − 1 {y_0},...,{y_{N - 1}} y0​,...,yN−1​&#xff1a; y k ≡ 1 N ∑ j 0 N − 1 x j …

【初识C语言】变量和常量

文章目录 1. 局部变量和全局变量2. 变量的作用域和生命周期3. 常量 生活中的有些值是不变的&#xff08;比如&#xff1a;圆周率&#xff0c;性别&#xff0c;身份证号码&#xff0c;血型等等&#xff09;有些值是可变的&#xff08;比如&#xff1a;年龄&#xff0c;体重&…

18款奔驰S450 4MATIC升级发光出风口,提升车内氛围感

完美匹配&#xff0c;全部都是原装位&#xff0c;安装很快&#xff0c;瞬间发光。随着氛围灯颜色的变化而变化。美丽靓丽与质感同存&#xff0c;大大提升了车的颜值档次。让您车生活更加的富有乐趣与满足&#xff01;

决策树笔记

决策树模型学习过程 1.根节点选择什么特征 2.只关注决策树的左侧分支 3.再关注右侧分支 纯度 熵就是样本的混乱程度&#xff0c;熵越大&#xff0c;越混乱&#xff0c;纯度就越低 减小熵信息增益 选择拆分信息增益 选择信息增益大的&#xff0c;以增加纯度 p1left 定义为…

2023年功能测试会被淘汰?自动化测试到测试开发?我的测试之路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 现在2023年&#…

数据技术在金融行业有哪些应用_光点科技

随着信息技术的迅猛发展&#xff0c;大数据技术逐渐成为金融行业的重要工具。大数据技术的应用&#xff0c;不仅可以提高金融机构的运营效率&#xff0c;还能够提供更准确的风险评估和预测&#xff0c;从而为投资者和决策者提供更好的决策依据。 那么&#xff0c;大数据技术在…

ES应用风暴

文章目录 使用案例启发使用ElasticSearch作为主要的后端&#xff0c;作为搜索引擎在现有系统增加ElasticSearch使用ElasticSearch和现有的工具 常用于日志收集&#xff08;分析&#xff09;系统、实时警报 使用案例 维基百科&#xff1a;全文检索、高亮、搜索推荐。The Guar…