ITPub博客

首页 > 数据库 > 数据库开发技术 > 锁升级带来的对象变化

锁升级带来的对象变化

原创 数据库开发技术 作者:安瑞哥是码农 时间:2022-03-30 20:02:28 0 删除 编辑



前言


作为Java程序员或者大数据开发工程师,对于线程锁,应该早就司空见惯。

其实我一直不太喜欢[锁]这个词,因为很容易跟现实中的物理锁联系起来而产生混淆,用[资源独占状态]似乎更好理解一点,它表示的是对象被占用的一种[状态],本质就是在这个[被占用对象]的某个位置(Markword)打上标记而已,并不存在一个[额外的锁对象]。

程序锁的存在,是因为有线程竞争,当多个线程同时向同一个资源进行争夺时,于是就需要建立一种规范,来让竞争者有序地对资源进行合理的利用。

而针对这种规范,不同的竞争场景,为了考虑效率,其约定的方式也会不太一样,于是在程序运行过程中,随着竞争者的增加,其锁状态也随之[升级加码],于是就有了[锁升级]这个过程。

本文将围绕Java语言中常用的synchronized关键字,通过代码实践来聊聊锁升级的过程。





|锁的必要性


对于多线程来操作单个资源来说,因为资源[独一份]的特性,
而如果想要对这个独一份的资源合理有效的利用,那么当多个线程对其修改时,就需要加锁。

这个就像现在排队做核酸,护士小姐姐就1个人(资源独一份),要做核酸的人那么多(多线程),那怎么办?只能排队,当前面的人在张开嘴巴接受护士小姐姐戳喉咙的时候(拥有线程锁),后面的人就只能等,当前面的人戳完喉咙离开时(释放锁),才能轮到下一个。

排队做核酸与锁的关系

试想,如果这个时候大家都想一睹护士小姐姐的芳容,都一窝蜂跑到她跟前,大家都张开嘴巴要求[捅喉咙],那护士小姐姐估计会被你吓的花容失色,整个场面估计也会混乱不堪。

所以锁的本质就是[排队],就是[单线程],是为了让单个资源得到有效、合理的利用而采取的一种[规范]和[约束]手段。

但是,如果仅仅只是一种排队策略的话,似乎并不能满足程序多样性的需求,为了满足不同的锁竞争场景,需要设计不同的排队策略——锁升级。

比如有的人排队时候,眼睛就一直盯着前面的人看做完没有,计算还有多久轮到自己——自旋锁,也叫轻量级锁。

也有的人看前面排队人多,干脆低头玩起了手机,对排队进度不再关心,直到护士小姐姐告诉他到了——重量级锁。




|锁的升级


为什么不直接设计一种锁机制,搞那么多不嫌麻烦吗?

根本原因在于提高程序的运行效率。

其实早在JDK1.2之前Java的锁关键字synchronized是没有轻量级和重量级之分的,全部统一都是重量级锁,效率很低。

试想,当大家在排做核酸时,所有人只要进到排队的人群中就开始低头玩手机,也不看排队进度,都等着护士小姐姐挨个叫说:嘿!哥们,到你了,快点的吧(重量级锁)。护士小姐姐得多累,整个做核酸的效率得多低。

于是JDK后来的版本考虑到这一点,才有了锁升级的过程,锁的升级过程一共分为下面3个阶段:


为了完整演示出这个3个不同的锁升级过程,经过不同的参数调整,整理出下面这样一份代码(别看就这么点东西,费老劲了),你可以通过控制不同的参数和线程数来得出对象在不同锁状态下的样子:

public class SynchronizedTest {
    private static Object object = new Object();

    /**打印出被锁的对象*/
    private static void print() throws InterruptedException {
        synchronized (object) {
            System.out.println("####################################");
            Thread.sleep(1000);//为了加剧线程间的竞争
            System.out.println(ClassLayout.parseInstance(object).toPrintable());//打印对象被锁之后布局
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //调用synchronized之前
        System.out.println(ClassLayout.parseInstance(object).toPrintable());//打印对象锁之前的布局
        //调用synchronized之后,单个线程没有竞争
        print();
        //有若干个线程竞争情况下
        for (int i = 0; i < Integer.parseInt(args[0]); ++i){
            new Thread(() -> {
                try {
                    print();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

另外,一个对象的锁处于哪个状态,是写在其对象的Markword中的,对于一个对象的Markword的布局如下图所示:


而锁状态就看红色标记的那3个或2个bit位,其他地方先不用管,不是本文重点,其值跟锁的对应关系如下表:

偏向锁标志

锁标志

对应锁状态

0

01

无锁

1

01

偏向锁

-

00

轻量级锁

-

10

重量级锁

-

11

GC标志



1. 偏向锁

所谓偏向锁?就是该锁偏向某一个线程,那什么情况下会偏向呢?就是只有一个线程对单个资源进行操作时,此时没有其他线程跟你竞争,那么此时,这个锁是偏向你的,即只被你使用。


类比到做核酸就是,刚好只有你一个人,那还排什么队啊,直接去护士小姐姐面前坐下就好了,此时的小姐姐只归你一人所有。


这段代码展示了一个对象从没有被加锁,到加锁后的状态(结果按理说应该是从无锁状态,升级到偏向锁状态),看下运行结果:



为了方便演示出偏向锁的效果,这里需要借助参数-XX:BiasedLockingStartupDelay,意思是对象的偏向锁启动延迟,JDK默认是4s,这里为了方便演示出效果,改为0,即不等待。

发现没?这个对象无论是在加锁前还是加锁后,其锁标记位都是101,即都是偏向锁,跟我们的预期并不符(因为偏向锁的延迟设置为0导致的)。


而当我把偏向锁延迟参数改为100ms时,再看一下运行效果:



可以看到,对象在加锁前打印出来为[无锁]状态,而加锁之后,瞬间就升级到了[轻量级锁]状态。


因此可以证明,锁的升级过程并不是严格按照:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,这种顺序来的。



2. 轻量级锁


也叫自旋锁——CAS(compare and swap),从字面翻译叫做:比较并交换。


它是一种乐观锁的实现,所谓乐观锁,就是[总以为自己随时可以],比如在排队做核酸的时候,别人都在低头玩手机等,而你却总是盯着最前面看,总以为下一个就到自己了,所以你一直都不闲着。当排队人数比较少时候,用这招比较管用,效率很高。


之所以叫轻量级锁,是因为这部分是由JVM自己来管理。


为了演示出轻量级锁的效果,我们增加1个额外的线程参与竞争,来看看运行效果:


可以看到,对象的锁状态位由101变成了00,即偏向锁升级成了轻量级锁。


轻量级锁只有在线程竞争不激烈的情况下启用,那么什么时候变成重量级锁呢?



3. 重量级锁


之所以叫重量级,是因为这个时候排队的线程不再傻傻的一直盯着被占用的资源看有没有被释放,因为这个时候如果竞争的线程太多,你这样一直盯着太费眼了(浪费CPU资源)。


于是,这个时候就想出来一个比较节省资源的办法——进入等待队列,而这个队列是由操作系统来维护的,因此,这里面有个用户态向内核态切换的过程,这也是为什么叫重量级锁的原因,你都惊动了操作系统老大了,能不重吗?


这里面其实有个悖论,那就是锁之所以从轻量级升级到重量级,目的在于节省资源,但是重量级锁又是我们工作中要尽可能避免的,只是这个避免不是要我们不要用重量级锁,而是通过合理的程序设计和合理的资源分配,来尽量减少线程间的竞争。


因为当前我使用的是JDK1.8,想要实现轻量级锁到重量级锁的升级,加大竞争的线程数就够了。

JDK之前版本默认是单个线程循环10次后如果还没有获得锁,那么从轻量级锁升级为重量级锁,或者等待的线程数超过CPU核数的一半即升级。

但是JDK1.6之后,改成了自适应自旋,也就是JVM自己根据当前运行环境来判断什么时候将轻量级锁升级为重量级锁


来,通过增加竞争的线程数量,我们来看一下效果:



当我把竞争的线程数量加到3之后,发现对象的锁状态位由101变成了10,即偏向锁升级成了重量级锁。




结语

——

本文从实践验证的角度来证明程序锁的升级过程,告诉你被锁对象在不同的锁状态下其对象本身的细微差异。


虽然这是个非常细的知识点,但是在这个程序员越来越卷的时代,经常成为一些大厂的面试必考题。

因此,想要立于不败之地,咱必须也得卷起来才行。





因公众号无法留言,如果你觉得我讲的还凑合或者有任何疑问,欢迎找我交流探讨...



原文作者:安瑞哥是码农;如有侵权,请联系管理员删除。

上一篇: 关于Java指针压缩
请登录后发表评论 登录
全部评论

注册时间:2022-02-16

  • 博文量
    228
  • 访问量
    115457