synchronized锁升级过程

1、Java中的对象头

普通对象
image.png
数组对象
image.png
Mark Word结构
image.png
Mark Word:
Java对象头中的Mark Word(标记字)是Java虚拟机为每个对象额外存储的一部分数据,用于辅助GC(垃圾回收)和线程同步等操作。Mark Word位于对象的内存布局最开始的位置。
Mark Word中常见的信息,对象的Hashcode、分代年龄、锁信息、对象标志位等。
Klass Word:
Klass Word可以被视为一个指针,它存储了关于类的元数据信息,包括类的类型、父类、方法表等。

2、Monitor(锁监视器、管程)

Monitor是由操作系统生成的对象,和Java中的对象一一对应,在JDK1.6锁优化之前,被synchronized修饰的对象,对象头的Mark Word会存放指向Monitor的指针,而Monitor会将Owner置为该线程。
image.png
在Thread-2未解锁之前,其他线程访问同步代码块,会进入EntryList阻塞队列中,当锁释放了才会唤醒。

3、字节码层面原理

代码:

public static void main(String[] args) {
    Object lock = new Object();
    synchronized (lock) {
        System.out.println("ok");
    }
}

字节码:

0: 	new				#2		// new Object
3: 	dup
4: 	invokespecial 	#1 		// invokespecial <init>:()V,非虚方法
7: 	astore_1 				// lock引用 -> lock
8: 	aload_1					// lock (synchronized开始)
9: 	dup						// 一份用来初始化,一份用来引用
10: astore_2 				// lock引用 -> slot 2
11: monitorenter 			// 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic 		#3		// System.out
15: ldc 			#4		// "ok"
17: invokevirtual 	#5 		// invokevirtual println:(Ljava/lang/String;)V
20: aload_2 				// slot 2(lock引用)
21: monitorexit 			// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
25: astore_3 				// any -> slot 3
26: aload_2 				// slot 2(lock引用)
27: monitorexit 			// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 		any
25 28 25 		any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 	31 		0 args [Ljava/lang/String;
8 	23 		1 lock Ljava/lang/Object;

11-21是代码块加锁解锁过程
25-27是发生异常情况后也会释放锁

4、锁升级过程

Java中的锁升级可以分为
无锁 -》 偏向锁 -》 轻量级锁 -》重量级锁

4.1 偏向锁

当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作。
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低
  • 当一个对象已经计算过 hashCode,就再也无法进入偏向状态了
  • 添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

撤销偏向锁的状态:

  • 调用对象的 hashCode:偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 导致偏向锁被撤销
  • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 调用 wait/notify,需要申请 Monitor,进入 WaitSet

在这里插入图片描述

4.2 轻量级锁

当多个线程竞争同一个锁时,JVM将锁从偏向锁态升级到轻量级锁态。在轻量级锁中,Mark Word中记录了指向锁记录(Lock Record)的指针。线程在获取轻量级锁时,会使用CAS(Compare and Swap)操作将Mark Word替换成指向自己的锁记录,如果CAS成功,则说明获取轻量级锁成功,可以进入同步块进行操作。如果CAS失败,则表示锁竞争失败,需要升级为重量级锁。

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

让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

  • 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁
  • 如果 CAS 失败,有两种情况:
    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数

  • 当退出 synchronized 代码块(解锁时)
    • 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
    • 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

4.3 重量级锁

在这里插入图片描述

Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

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

5、锁优化

5.1 自旋锁

在重量级锁竞争的时候,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁
注意:

  • 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
  • 自旋失败的线程会进入阻塞状态

优点:不会进入阻塞状态,减少线程上下文切换的消耗
缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源

  • 自旋成功

  • 自旋失败

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dn4xr4x4-1692460959963)(https://cdn.nlark.com/yuque/0/2023/png/26767467/1692460367690-98845215-71b9-4ba0-8712-53664d4f4064.png#averageHue=%23f4f3f2&clientId=u218fc4ab-d702-4&from=paste&id=uac72a52e&originHeight=480&originWidth=852&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u8280d567-9138-448a-88d2-dc052bb6bb3&title=)]

自适应自旋锁:

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
  • Java 7 之后不能控制是否开启自旋功能,由 JVM 控制
//手写自旋锁
public class SpinLock {
    // 泛型装的是Thread,原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " come in");

        //开始自旋,期望值为null,更新值是当前线程
        while (!atomicReference.compareAndSet(null, thread)) {
            Thread.sleep(1000);
            System.out.println(thread.getName() + " 正在自旋");
        }
        System.out.println(thread.getName() + " 自旋成功");
    }

    public void unlock() {
        Thread thread = Thread.currentThread();

        //线程使用完锁把引用变为null
		atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + " invoke unlock");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLock lock = new SpinLock();
        new Thread(() -> {
            //占有锁
            lock.lock();
            Thread.sleep(10000); 

            //释放锁
            lock.unlock();
        },"t1").start();

        // 让main线程暂停1秒,使得t1线程,先执行
        Thread.sleep(1000);

        new Thread(() -> {
            lock.lock();
            lock.unlock();
        },"t2").start();
    }
}

5.2 锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)

5.3 锁粗化

对相同的对象多次加锁,频繁的加锁操作会导致性能消耗,可以通过锁粗化的方式将加锁的返回扩大,减少加锁次数。

5.4 多把锁

将锁的粒度细分,增加并发度。

  • 好处:增加并发度
  • 坏处:可能出现死锁
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小C卷Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值