synchronized底层是怎么通过monitor进行加锁的?

一、monitor是什么

monitor叫做对象监视器、也叫作监视器锁,JVM规定了每一个java对象都有一个monitor对象与之对应,这monitor是JVM帮我们创建的,在底层使用C++实现的

ObjectMonitor() {

    _header;

    _count ; // 非常重要,表示锁计数器,_count = 0表示还没人加锁,_count > 0 表示加锁的次数

    _waiters;

    _recursions;

    _owner; // 非常重要,指向加锁成功的线程,_owner = null 时候表示没人加锁

    _waitset; // wait线程的集合,在synchorized代码块中调用wait()方法的线程会被加入到此集合中沉睡,等待别人叫醒它

    _waitsetLock;

    _responsiable;

    _succ;

    _cxq;

    _freenext;

    _entrylist; // 非常重要,等待队列,加锁失败的线程会被加入到这个等待队列中,等待再次争抢锁

    _spinFreq; // 获取锁之前的自旋的次数

    _spinclock; // 获取之前每次锁自旋的时间

    ownerIsThread;

}
二、monitor对象的关键属性

_count : 这个属性非常重要,直接表示有没有被加锁,如果没被线程加锁则 _count=0,如果 _count大于0则说明被加锁了

_owner:这个属性也非常重要,直接指向加锁的线程,比如线程A获取锁成功了,则 _owner = 线程A;当 _owner = null的时候表示没线程加锁

_waitset:当持有锁的线程调用wait() 方法的时候,那个线程就会释放锁,然后线程被加入到monitor的waitset集合中等待,然后线程就会被挂起。只有有别的线程调用notify将它唤醒

_entrylist:这个就是等待队列,当线程加锁失败的时候被block住,然后线程会被加入到这个entrylist队列中,等待获取锁。

_spinFreq:获取锁失败前自旋的次数;JDK1.6之后对synchronized进行优化;原先JDK1.6以前,只要线程获取锁失败,线程立马被挂起,线程醒来的时候再去竞争锁,这样会导致频繁的上下文切换,性能太差了。JDK1.6后优化了这个问题,就是线程获取锁失败之后不会被立马挂起而是每个一段时间都会重试去争抢一次,这个 _spinFreq就是最大的重试次数也就是自旋的次数,如果超过了这个次数抢不到,那线程只能沉睡了。

_spinClock:上面说获取锁失败每隔一段时间都会重试一次,这个属性就是自旋间隔的时间周期,比如50ms,那么就是每隔50ms就尝试一次获取锁。

三、那么这些属性是怎么进行加锁的呢?

(1)首先,没有线程对monitor进行加锁的时候是这样的:

_count = 0 表示加锁次数是0,也就是没线程加锁; _owner 指向null,也就是没线程加锁

(2)然后,这个时候线程A、线程B来竞争加锁了,如下图所示:

(3)线程A竞争到锁,将 _count 修改为1,表示加锁次数为1,将_owner = 线程A,也就是指向自己,表示线程A获取到了锁。

(4)那反过来推测,释放锁的时候是不是将_count 设置为 0 , 将 _owner 设置为 null 就 OK了?对的

四、那monitor的其他属性是用来干啥的呢?

上面说过 _spinFreq是等待锁期间自旋的次数_spinclock是自旋的周期也就是每次自旋多久时间、 _entrylist这个就是自旋次数用完了还没获取锁,只能放到 _entrylist等待队列挂起了。如下图:

(1)首先线程B获取锁的时候发现monitor已经被线程A加锁

(2)然后monitor里面记录的 _spinFreqspinclock 信息告诉线程B,你可以每隔50ms来尝试加锁一次,总共可以尝试10次

(3)如果线程B10次尝试加锁期间,获取锁成功了,那线程B将 _count 设置为 1_owner 指向自己表示自己获取锁成功了

(4)如果10次尝试获取锁此时都用完了,那没辙了,它只能放到等待队列里面先睡觉去了,也就是线程B被挂起了。

五、获取锁失败后的自旋操作

为啥线程B请求失败之后不直接进入队列挂起?而是要自旋之后再次尝试获取锁?为啥不是一直自旋然后尝试获取锁,而是要设置一个最大尝试次数?

这个啊,其实跟jvm获取monitor锁的优化有关,这么做有什么好处呢?

(1)首先跟你说下,线程挂起之后唤醒的代价很大,底层涉及到上下文切换,用户态和内核态的切换,我打个比方可能最少耗时3000ms这样,这只是打个比方哈

(2)线程A获取了锁,这个时候线程B获取失败。按照上面自旋的数据 _spinclock = 50ms(每次自旋50ms), _spinFreq = 10(最多10次自旋)

(3)假如线程A使用的时间很短,比如只使用150ms的时间;那么线程B自旋3次后就能获取到锁了,也就花费了150ms左右的时间,相比于挂起之后唤醒最少花费3000ms的时间,是不是大大减少了等待时间啊......,这也就提高了性能了。

(4)如果不设置自旋的次数限制,而是让它一直自旋。假如线程A这哥们耗时特别的久,比如它可能在里面搞一下磁盘IO或者网络的操作,花了5000ms!!。

线程B可不能在那一直自旋着等着它吧,毕竟自旋可是一直使用CPU不释放CPU资源的,CPU这时也在等着不能干别的事,这可是浪费资源啊,所以啊自旋次数也是要有限制的,不能一直等着,否则CPU的利用率大大被降低了。

所以在10次自旋之后,也就是500ms之后,还获取失败,那就把自己挂起,释放CPU资源咯

六、monitor的wait和notify

说起monitor里面的waitset,上面讲的就是一个集合

必须是当线程获取锁之后,才能调用wait()方法,然后此时释放锁,将_count恢复为0,将_owner指向 null,然后将自己加入到waitset集合中等待别人调用notify或者notifyAll将其中waitset的线程唤醒。

那notify和notifyAll有啥区别啊?

简单说就是notify就是从waitset随机挑一个线程来唤醒,只唤醒一个。notifyAll这方法就是将waitset中所有等着的线程全部唤醒了。

假如说现在有个场景是这样的:

线程A执行如下代码:

synchronized(this) {
    if (某个条件) {
        wait();    
    }    
}

线程B执行如下代码:

synchronized(this) {
    // 某些业务逻辑
    ......
    notify();
}

下面画个图来说一下:

(1)首先啊还是线程A这哥们动作比较快,先获取到了锁

(2)然后线程A发现条件不满足,想了想,算了,我先释放锁,睡个觉,等条件满足了,别人再唤醒我,岂不是美滋滋。于是释放了锁,睡觉去了

(3)然后线程B自己可以加锁了,执行了一些业务逻辑,然后去调用notify方法唤醒线程A,嘿兄弟,别睡了,到你了...

(4)线程A醒来之后还是要再去去竞争锁的,也就是醒来之后还要竞争将_count修改为1,竞争_owner指向自己毕竟它还在synchronized代码块内部嘛,只有获取锁之后才能执行synchronized代码块的代码。所以只有它再次获取到锁了之后,才会执行代码块内部的逻辑。

因为waitset集合是monitor对象的一个属性,所以调用之前必须要获取到monitor对象的操作权限,也就是获取到锁,notify要操作waitset也是一样。

### Java 中 `synchronized` 的可重入性原理 `synchronized` 是 Java 提供的一种内置机制,用于实现线程间的同步控制。它具备 **可重入性** 特性,这意味着当某个线程已经获取到对象的后,在该线程内部可以再次调用同一对象上的其他被 `synchronized` 修饰的方法而不会发生死。 #### 可重入性的定义与表现 在一个类中,如果一个同步方法 A 调用了另一个同步方法 B,则当前线程无需重新申请即可进入方法 B。这种行为表明了 `synchronized` 的可重入特性[^2]。例如: ```java public class RecursiveLockCondition2 { private synchronized void method1() { System.out.println("method1 执行"); method2(); } private synchronized void method2() { System.out.println("method2 执行"); } public static void main(String[] args) { RecursiveLockCondition2 condition2 = new RecursiveLockCondition2(); condition2.method1(); // 输出:method1 执行 -> method2 执行 } } ``` 上述代码展示了在同一对象实例上,`method1()` 和 `method2()` 都是同步方法,但在 `method1()` 内部调用 `method2()` 不会引发死,这正是由于 `synchronized` 的可重入性所致。 #### 实现原理 `synchronized` 的底层实现依赖于 JVM 的监视器(Monitor),其核心数据结构是一个计数器——**持有的次数** 计数器。每当线程成功获得时,计数器加一;每次释放时,计数器减一。只有当计数器降为零时,才会真正被释放并允许其他线程竞争资源[^4]。 具体过程如下: - 当线程首次尝试获取某对象的时,JVM 将此线程标记为该的所有者,并初始化计数器为 1。 - 如果同一线程再次请求相同的(即在已持有的范围内调用另一同步方法),则计数器增加而不阻塞线程。 - 每次退出同步代码块或方法时,计数器减少一次。 - 当计数器归零时,表示已被完全释放,此时其他等待中的线程有机会获取该。 通过这种方式,`synchronized` 支持了多层嵌套调用的安全性和一致性,从而实现了可重入功能。 #### 对象加锁的行为 尽管某些资料可能提到 “Java 中对象加锁不具有可重入性”,但实际上这是误解。只要遵循正确的编程模式,`synchronized` 自然支持可重入定[^3]。因此,任何关于不可重入的说法都需要谨慎对待,通常是因为特定场景下的错误使用而非机制本身的缺陷。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Leighteen

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

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

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

打赏作者

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

抵扣说明:

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

余额充值