偏向锁,轻量级锁,重量级锁的核心原理

举报
酸菜鱼. 发表于 2022/10/31 19:55:03 2022/10/31
【摘要】 Java对象结构在讲到本文的锁之前,先来简单了解一下Java的对象结构。Java的对象结构主要包括对象头,实例数据,对齐填充三大部分。 对象头对象头中存储了对象的Mark word,类型指针(元数据指针)和数组长度(只有当前对象为数组对象时才会有)。而Mark word又包括对象的Hashcode码,对象的分代年龄,对象的偏向锁ID,获取偏向锁的时间戳,锁标志位等。Mark word主要用...

Java对象结构

在讲到本文的锁之前,先来简单了解一下Java的对象结构。Java的对象结构主要包括对象头,实例数据,对齐填充三大部分。
在这里插入图片描述

对象头

对象头中存储了对象的Mark word,类型指针(元数据指针)和数组长度(只有当前对象为数组对象时才会有)。而Mark word又包括对象的Hashcode码,对象的分代年龄,对象的偏向锁ID,获取偏向锁的时间戳,锁标志位等。

Mark word主要用于存储对象自身运行时的数据,并且Mark word字段的长度与JVM的位数有关,32位的JVM虚拟机中Mark word占用32位的存储空间,64位的JVM虚拟机中占用64位的储存空间

以32位的JVM虚拟机为例:
在这里插入图片描述

是否是偏向锁表示:当值为0时,标记对象没有开启偏向锁,值为1表示开启了偏向锁。

锁标志位表示:当前线程拥有的锁,不同状态下拥有的锁不同。

对象分代年龄表示:当JVM发生GC垃圾回收时,新生代未被回收的对象在Eden区和Survivor之间的复制,每次复制是的分代年龄+1,默认情况下分代年龄达到15会被移到老年代区域,当然这个参数也可以自行设置。

对象的Hashcode值:主要存储对象的Hashcode值。

线程ID:表示在偏向锁状态下,持有偏向锁的线程编号。

指向栈中锁记录的指针:在轻量级锁状态下,指向栈中所记录的指针。

指向(互斥量)重量级锁的指针:在重量级锁状态下,指向对象监视器的指针。

类型指针指向方法区中的类元信息,也叫元数据指针。

对于数组长度,在当前对象是数组类型的时候,对象头中需要额外的空间存储数据长度的信息,如果当前对象不是数组类型的,则不需要。同样,在32位的虚拟机中,数组长度占用32位的存储空间,64位的虚拟机中占用64位的存储空间。

实例数据

实例数据主要存储了对象的成员变量信息,比如成员变量的具体值,父类成员变量的具体值等。

对象填充

在HotSpot虚拟机中,对象的起始地址必须为8的整数倍,如果当前对象的实例变量占用的储存空间不是8的整数倍,则需要使用填充数据来保证对齐。

偏向锁

为什么会产生偏向锁?在多线程并发执行时,synchronized锁会保证安全性能,但是同时会出现一个线程多次获取同一个锁的现象(哎,就是玩😎),因此提出了偏向锁。

偏向锁是如何工作的?如果在同一时刻,只有一个线程拿到了synchronized锁,此时该线程执行方法或代码块不会出现与其他线程竞争的情况,这时,会进入偏向状态。当锁进入偏向状态,对象头中的Mark word就会进入偏向结构,由上文得知,偏向锁标记为1,锁标志位标记为01,并且会把当前拿到锁的线程ID记录到对象头的Mark word中,一旦下次改线程进入临界区(方法或代码块),那么会先检查Mark word中存储的ID和自己的是否一致。

如果发现Mark word中存储的线程ID和自己的一致,则可以进入临界区,如果不一致,说明有线程与当前拿到锁的线程竞争。假如线程1正在使用锁资源,线程2发现不一致,则线程2会尝试使用CAS机制将对象头Mark word中的线程ID改为线程2自己的,然而又会分为两种情况:

1.当CAS操作执行成功后,表示线程1已经结束了使用锁资源,Mark word中的线程ID会记录为线程2的ID,此时仍然处于偏向锁状态;

2.当CAS操作执行失败,表示线程1仍然在占用锁资源,没有释放锁资源,此时会暂停线程1,并且将Mark word中偏向锁的值设为0,锁标志位设为00,由上文可知,此时偏向锁会升级为轻量级锁,当然,线程1和线程2之间会按照轻量级锁的方式来竞争锁。

偏向锁会提升程序的执行性能,但是偏向锁的撤销升级是比较复杂的,并且会消耗资源和性能。

轻量级锁

轻量级锁概念是在线程竞争不是很激烈时,可以通过CAS机制来竞争锁,避免使用操作系统层面的Mutex重量级锁从而影响性能。

轻量级锁如何实现呢?当线程被创建后,虚拟机会在线程的栈帧中创建一个用于存储锁记录的空间–Displaced Mark word,对于轻量级锁,在争抢锁资源的线程进入synchronized修饰的方法或代码块之前,会将锁对象中对象头里的Mark word复制到当前线程的Displaced Mark word空间中,然后,线程会尝试使用CAS自旋的方式将锁对象中的Mark word替换成指向锁记录的指针,替换成功则代表当前线程拿到了锁,之后虚拟机会把Mark word中的锁标志位设为00,表示当前为轻量级锁状态,当前线程获取到锁之后,JVM虚拟机会把锁对象中Mark word中的信息保存到获取到的锁资源的线程的栈帧Displaced Mark word中,并且把线程中的owner指针指向锁对象。

当线程抢占到锁资源后,会将锁对象的Mark Word中的信息保存到当前线程栈帧中Displaced Mark World区域,而且锁对象的Mark Word信息也会发生变化,由之前的存储对象的HashCode码到现在变成存储指向栈中锁记录的指针。当线程释放锁时,会尝试使用CAS操作将Displaced Mark Word中存储的信息复制到锁对象中的Mark Word中,如果没有发生锁竞争,则代表复制成功,线程会释放锁,如果此时由于其他线程多次执行CAS操作导致轻量级锁升级为重量级锁,则当前线程的CAS操作会失败,此时会释放锁并唤醒其他未获得锁而被阻塞的线程同时竞争锁。

重量级锁

重量级锁主要基于操作系统中的Mutex锁实现的,重量级锁的执行效率比较低,处于重量级锁时被阻塞的线程不会消耗CPU资源。
它的底层是通过Monitor锁实现等待,如果当前对象锁的状态为偏向锁或轻量级锁,那么在调用锁对象的wait方法或notify方法,或者计算锁对象的HashCode时,偏向锁或轻量级锁就会膨胀为重量级锁。

锁升级

在多线程同时争抢锁时,可能会由无锁状态慢慢升级为轻量级锁,重量级锁的情况,升级过程如下:

在多线程竞争锁时,虚拟机会检测锁对象头中的Mark Word偏向锁标记是否为1,锁标记位是否为01,如果是的话,则当前锁状态为可偏向状态;

多线程争抢锁资源时,会首先检查Mark Word中存储的是不是自己线程的ID,如果是自己线程的话,则已经处于偏向锁状态了,当前的线程可以直接进入方法或代码块中;

如果存储的不是自己的线程,那么当前竞争锁的线程会使用CAS自旋机制来竞争锁,如果竞争成功,则会把Mark Word中存储的线程ID改为当前竞争锁成功的ID,并且把偏向锁标记设为1,锁标志位设为01,此时锁状态仍然处于偏向锁状态;如果当前线程通过CAS自旋操作竞争失败,则说明有其他线程也在争抢锁资源,那么此时会撤销偏向锁,将偏向锁升级为轻量级锁;

当前线程通知锁对象的Mark Word中存储的线程ID暂停线程,对应的线程会将Mark Word的内容变为空;

上次拿到偏向锁资源的线程(线程1),和当前争抢锁的线程(线程2)都会把锁对象中的HashCode值等信息复制到自己栈帧中的Displaced Mark Word中,之后线程1和线程2开始执行CAS自旋操作,尝试把锁对象中的Mark Word的内容修改为指向自己线程的Displaced Mark Word的空间来竞争锁资源;

竞争成功的线程会拿到锁资源,并且竞争成功的锁的线程会把锁对象中的Mark Word的内容修改为指向自己线程的Displaced Mark Word的空间,并将Mark Word中的锁标志为设为00,进入轻量级锁状态;

当时竞争失败的锁不会灰心的,仍然会继续CAS自旋的操作来竞争锁资源,此时又会分为两种情况:如果竞争成功,则会拿到轻量级锁,注意是轻量级,此时锁仍然会处于轻量级锁状态;如果竞争失败,线程一直进行CAS自旋操作达到一定的次数仍然没有拿到锁资源,那么轻量级锁会膨胀为重量级锁,将Mark Word中的锁标志位设置为10进入重量级锁状态。

因此,综上所述,当同一时刻,只有一个线程竞争锁时,此时会处于偏向锁状态;如果有多个线程同时竞争锁时,偏向锁会升级为轻量级锁状态;当线程以CAS自旋机制超过了一定的自旋次数,仍然没有获取到锁,会由轻量级锁升级为重量级锁状态。

锁消除

只有Java虚拟机开启了逃逸分析,才会出现锁消除的现象,意思是当一个对象只能从一个线程被访问到,不存在共享数据的竞争,在访问这个对象时,可以不加同步锁,如果使用了synchronized同步锁,则虚拟机会自动将synchronized同步锁消除,常见的例子有StringBuffer拼接字符串:

public static String add(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1);
        sb.append(str2); 
        return sb.toString();   
        }

StringBuffer中的append()方法源码:

    public synchronized StringBuffer append(StringBuffer sb) {
        toStringCache = null;
        super.append(sb);
        return this;
    }

上述代码中,总所周知StringBuffer是线程安全的,其方法被synchronized锁修饰,虽然存在锁,但是在执行上述代码时,其他线程无法访问到共享数据,因此虚拟机会将synchronized锁安全消除。

本篇文章就分享到这里了,后续将会分享各种其他关于并发编程的知识,感谢大佬认真读完支持咯 ~

文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起讨论🍻
希望能和诸佬们一起努力,今后进入到心仪的公司
再次感谢各位小伙伴儿们的支持🤞

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。