知识串联之synchronized锁住了谁?(七)

61b0110820b95724ac74da66846bbd08.png

前言

上篇文章详细介绍了Java对象模型和对象头的组成,并在介绍Mark Word时引入了synchronized关键字,对其做了简单的介绍;本文我们通过synchronized关键字来深入研究下,它是如何使用Mark Word实现的锁升级、以及各种维度的锁实现。

一个经典的synchronized面试题

/**
 * @author 程序反思录 <程序反思录@xx.com>
 * Created on 2024-10-18
 */
public class SignalObject {

 // 使用volatile修饰保证实例对象的内存可见性
    private static volatile SignalObject instance;

    // 私有化构造器
    private SignalObject() {
    }

    // 双重校验 + 锁机制
    public static SignalObject getInstance() {
        if (instance == null) {
            synchronized (SignalObject.class) {
                if (instance == null) {
                    instance = new SignalObject();
                }
            }
        }
        return instance;
    }
}

上面是一个使用volatilesynchronized组合,实现了一个懒汉式的单例对象的构建。这里synchronized使用SignalObject类的class对象作为锁,当多个线程同时同时获取实例时,只有第一个抢到类class锁的线程才会执行new SignalObject()初始化实例对象,别的线程获取的实例对象都是已经创建好的,从而使得SignalObject类只有一个实例。

synchronized锁住了谁?

上面的单例类实现的case,我们采用了SignalObject类Class作为锁对象,它的底层到底是怎么实现的呢?

我们通过javap -v SignalObject.class命令看一下字节码实现:

a88ee38539ce9d3f9a689479bed20383.png

通过上图可以看出,在执行获取实例对象业务逻辑的前后,有一对monitorentermonitorexit的标识,当JVM解析到这两个指令时,这两个字节码都需要一个Object类型的参数来指明要锁定和解锁的对象,我们在代码中指定了SignalObject.class作为锁定的对象,所以这里的锁就是SignalObject类。

下面这个case是将synchronized加到了方法上面,那么在没有明确指定的锁是谁的时候,此时根据synchronized修饰的是实例方法还是类方法,获取对应的实例对象或class对象作为锁对象。print1()方法对应的锁对象是class类,而print2()方法对应的锁对象是执行该方法的实例对象。

// synchronized 修饰在一个static方法上
public static synchronized void print1() {
    System.out.println("instance=" + instance);
}

// synchronized 修饰在一个非static方法上
public synchronized void print2() {
    System.out.println("instance=" + instance);
}

54d6b39f7bd9d8ce57238b4d892ddceb.png

实现原理

文章前半部分通过case以及字节码维度的实现了解了synchronized的基本使用,下面我们来分析synchronized的锁升级过程和monitor内部实现,进一步了解其底层实现原理。

锁升级

锁升级的过程分为偏向锁 -> 轻量级锁 -> 重量级锁,随着竞争锁的激烈程度逐渐加强,锁粒度也会逐渐膨胀。下面以获取单例方法的case为例

6c99a26216c80edc4ae08454b50c2ec4.png

偏向锁(基于JDK1.8讨论)

当获取单例对象的方法被首次访问时,JVM会对类class的对象头Mark Word中的是否偏向锁标志位设为1,对象头中的线程id设置为当前线程id,后续当当前线程再次获取实例对象时,会根据偏向锁标识和线程id进行对比,如果相同则由当前线程直接返回实例对象,这也是synchronized的可重入性的提现。在jdk15以后,Java官方废弃了偏向锁,这个在上篇文章中已经讲过,感兴趣的同学可以翻看前文

轻量级锁

当多个线程同时请求获取实例对象,由于产生了竞争,此时锁的粒度膨胀为轻量级锁,具体的实现方式是以CAS一定次数来获取,注意 通过源码分析可以得出,一旦CAS获取锁失败,就会直接膨胀到重量级锁。此时Mark Word指向线程栈中 lock record指针,表示在多个线程开始竞争,此时的竞争程度还不太激烈,该指针指向了获取锁成功的线程。

重量级锁

当在轻量级锁维度中抢锁依旧失败,此时就会进一步升级到重量级锁维度,在重量级锁维度时,通过monitor来实现抢锁的逻辑。此时对象头中Mark Word 指向一个monitor监视器的地址,具体的抢锁实现是通过monitor来实现。

monitor实现

解释一下ObjectMonitor的由来:Java中的每个Object对象在JVM内部都会对应一个C++对象oop,而oop内部有个markOop属性,markOop继承了oop并扩展了自己的monitor方法,这个方法返回一个ObjectMonitor指针对象,ObjectMonitor指针指向的ObjectMonitor就对应了Java对象头中的Mark Word

ObjectMonitor数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0;
    _recurlock    = 0; 
    _object       = NULL;
    _owner        = NULL; // 当前拥有锁的线程
    _WaitSet      = NULL; // 等待线程的集合
    _WaitSetLock  = 0;
    _Responsible  = NULL;
    _succ         = NULL;
    _cxq         = NULL;
    FreeNext      = NULL;
    _EntryList    = NULL; // 等待进入ObjectMonitor的线程集合
    _SpinFreq     = 0;
    _SpinClock    = 0;
    OwnerIsThread = 0;
}

可以看到ObjectMonitor中的属性非常多,不过我们只需重点关注如下几个属性即可:

  1. _owner: 表示当前在多线程竞争锁时,成功抢到锁对应的线程,即当前拥有执行业务逻辑权利的线程。

  2. _WaitSet: _WaitSet用于存放等待被唤醒的线程集合。当线程调用wait()方法时,释放当前持有的monitor,然后将_owner属性置为null,完成这些动作之后,最后将当前线程添加到_WaitSet集合中,然后等待后续某个合适的时机,将其从_WaitSet中唤醒。

  3. _EntryList: _EntryList用于存放等待锁而处于block状态的线程。当多线程竞争锁的时候,没有成功获取锁的线程会处于block状态,这些线程会临时存放到_EntryList中,等待已经成功获取锁的线程执行完业务逻辑并将锁释放掉后,然后通知_EntryList中等待锁的线程重新抢锁,成功抢到锁的线程从当前集合移除掉并去执行业务逻辑,而没有抢到锁的线程继续在该集合中等待。

我们通过一个图示来完整串联一下在重量级锁下,monitor内部是如何完成抢锁的全过程。

0bf54a804a2faaa866fcb43fecaa1d2e.png

  1. 线程请求获取锁,由于当前有等待获取锁的线程,即当前锁已经被某个线程占用,所以进入到Entry Set集合中等待抢锁;

  2. 当持有锁的Owner线程完成了业务处理,就会让出锁,并通知处于block状态的线程来抢锁,最终成功获得锁的线程从Entry Set进入到Owner中;

  3. 当持有锁的线程由于业务逻辑需要,执行了wait操作,就会将持有锁的线程从Owner中转移到Wait Set中,等待后续被唤醒;

  4. 当持有锁的线程执行了notify操作,则会从Wait Set中唤醒一个等待获取锁的线程重新回到Owner中,继续执行业务逻辑;

  5. 当持有锁的线程完成了所有业务逻辑的处理之后,让出锁让别的线程来继续执行执行1-4的步骤。

通过上面这个流程可以看出,由于任何对象都可以当做锁,也就解释了waitnotifynotifyAll等方法为什么被定义在Object类里面。

后续

本文从一个case入手,介绍了synchronized的基本使用,然后从字节码的角度进一步理解其实现原理,最后通过锁升级的过程,进一步深入到从Mark Word的角度理解了synchronized的实现过程。 

下篇文章接着串联下一个内存模型:JVM内存模型,通过理解JVM内存模型,来帮助我们在生产环境中遇到JVM类问题时(比如典型的如何快速处理线上OOM问题),能够处理的得心应手,敬请期待。

 做一个有深度的技术人

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值