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

2023年 9月 27日 74.6k 0

小陈:牛逼的老王,快来了,我的笔记本已经准备好了,开讲了......

老王:哈哈,好,搞起来......

老王:我们这一章节接着上一章的内容继续讲下去,本章讲解的是synchronized是怎么通过monitor进行重量级加锁?

老王:在讲synchronized是怎么通过monitor进行重量级加锁之前,我们先回顾一下上一章的那个Mark Word用途的表格:

image.png

当Mark Word的最后两位的锁标志位是10的时候,Mark Word这哥们说自己处于重量级锁的模式,重量级加锁不是它的责任,是monitor的责任,它作为mark word记录的数据是monitor的地址,让我们自己去找monitor去进行加锁。

image.png

老王:小陈啊,上一章说的对象头和Mark Word的知识点还记得吧,不记得要回去再回顾下哦。

小陈:哈哈,哪能啊,老王,上一章的知识点已经完全在我脑子里了。

上一章说了对象的基本结构,以及对象头中的Mark Word的含义,其中非常重要的就是当Mark Word中最后两位的锁标志位是10的时候,Mark Word的前面是monitor监视器的地址,我现在就给你画出来对象头、Mark Word 和 monitor之间的关系图:

image.png

老王:哈哈,就知道小陈你聪明,啥都记住了,那我也就不啰嗦了......

老王:那我下面就直接说monitor这玩意了......

monitor

老王:我先解释一下monitor是个啥东西:

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

老王:其实monitor在底层也是某个类的对象,那个类就是ObjectMonitor,它拥有的属性也字段如下:

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的一些属性,其中加锁非常重要的属性,你能看懂不?

小陈:知道monitor有哪些属性,怎么通过这些属性加锁的还是完全不懂啊......

image.png

老王:没关系,慢慢来;我首先给你解释一波有哪些关键的属性,然后跟你说怎么通过这些属性加锁的。

小陈:好滴,老王就是nice啊......

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,也就是没线程加锁

image.png

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

image.png

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

老王:讲到这里,聪明的小陈,你知道monitor加锁的原理是啥了没?

小陈:哈哈,老王,你上面刚刚把图画出来的时候,我就知道了。

在 _count = 0,_owner = null的时候,表示monitor没人加锁,这个时候线程A和线程B同时请求加锁,也就是竞争将_count改为1,由于线程A这哥们动作比较快,它将 _count改为1,获取锁成功了。它还嘚瑟了一下,同时将 _onwer = 线程A,表示自己获取了锁,告诉线程B,兄弟不好意思了,是我获取了锁,我先去操作了。

老王:哈哈,很好,小陈你理解能力越来越好了......

小陈:老王啊,既然加锁就是将 _count 设置为1,同时将 _owner 指向自己。

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

老王:没错,就是这样的,释放锁的过程就是这么简单......

image.png

小陈:加锁和释放锁我看懂了,那monitor的其它属性是用来干啥的?

老王:我们慢慢来,接着上面的_count和_owner属性,我们接下来将的是 _spinFreq、_spinclock、_entrylist这几个东西:

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

让我们继续接着图来讲:

image.png

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

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

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

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

老王:这样将上面的_spinFreq、_spinclock、_entryList这几个参数,小陈你听懂了没?

小陈:听懂了,也就是说_spinFreq和_spinclock 这两个monitor的属性主要是让线程自旋的时候使用的吧,而entryList作用是当线程自旋次数都用完了之后,只能进入等待队列进行休眠了,这样我就懂了......

老王:没错,就是这样......

小陈:牛逼啊,其实我还有个疑问啊,为啥线程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资源咯。

举例说明:

我就举个例子,假如有两个人要上厕所,但是只有一个坑位,线程A去得比较早,先把坑位给占了:

(1)假如线程A加锁了,它只是上了个小厕所,用了150ms就完成了;然后线程B尝试几次之后就能获取成功了

image.png

(2)但是如果线程A拉肚子了,这家伙在里面蹲了一个多小时....,线程B尝试了10次之后,发现坑还是没有空的。 这个时候线程B发现自己还有好多代码没写,害~,不等了,先释放CPU去写写代码,待会再来看看......

image.png

小陈:牛逼啊,老王,你这么一说我全懂了。你上班是不是经常去...,所以才这么深的体会......

老王:滚......

小陈:嘿嘿,我好想发现了什么......

老王:打住打住...,看看现在时间九点半了,给你讲完后面的_waitset我就回家了,我女儿还等着我回去呢...

小陈:好的,老王,那我们抓紧时间吧。

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();
}

下面画个图来说一下:

image.png

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

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

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

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

老王:小陈,我这么说,你听懂了没......

小陈:懂了懂了,原来是这样啊......

小陈:听了老王你的讲解,我终于知道wait和notify的原理了,也知道为啥要结合synchronized一起使用了。因为waitset集合是monitor对象的一个属性,所以调用之前必须要获取到monitor对象的操作权限,也就是获取到锁,notify要操作waitset也是一样。

所以wait和notify方法之后在获取了锁之后才能调用的,所以才需要写在synchronized方法块的内部啊,进入synchronized获取锁了之后才能执行啊

老王:没错,就是这样的......

小陈:听了老王你的讲解,之前网上的那些讨论我终于也明白了......

老王:什么讨论,说了听听

小陈:网上有人说wait() 和 Thread.sleep()的区别,说wait()会释放锁,而Thread.sleep()不释放锁,现在我终于知道了:

synchronized(this) {
    // 这个时候线程释放锁,然后将自己放入monitor的waitset队列,
    // 等待别人调用notify/notifyAll将唤醒
    wait(); 
}
synchronized(this) {
    // 这种情况不释放锁,就是睡个500ms然后醒来持有锁继续干活
    Thread.sleep(500);
}

老王:你说的没错,这是这样的...

小陈:哈哈,老王你真牛逼啊,懂得真多,这么底层的道理到了你这都使用很简单的方式讲明白了...

老王:得了得了,不吹牛逼,我该回去了,我女儿还等着我呢......

小陈:等等啊老王,我们下一章讨论啥啊?

老王:下一章《synchronized的锁升级原理?锁消除、无锁、偏向锁、轻量级锁、重量级锁、重量级锁自旋》,记得准时到场啊。

小陈:好的,老王,我们下一章见。

目录

JAVA并发专题 《筑基篇》

1.什么是CPU多级缓存模型?

2.什么是JAVA内存模型?

3.线程安全之可见性、有序性、原子性是什么?

4.什么是MESI缓存一致性协议?怎么解决并发的可见性问题?

JAVA并发专题《练气篇》

5.volatile怎么保证可见性?

6.什么是内存屏障?具有什么作用?

7.volatile怎么通过内存屏障保证可见性和有序性?

8.volatile为啥不能保证原子性?

9.synchronized是个啥东西?应该怎么使用?

10.synchronized底层之monitor、对象头、Mark Word?

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

12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁

13.synchronized怎么保证可见性、有序性、原子性?

JAVA并发专题《结丹篇》

  • JDK底层Unsafe类是个啥东西?
  • 15.unsafe类的CAS是怎么保证原子性的?

    16.Atomic原子类体系讲解

    17.AtomicInteger、AtomicBoolean的底层原理

    18.AtomicReference、AtomicStampReference底层原理

    19.Atomic中的LongAdder底层原理之分段锁机制

    20.Atmoic系列Strimped64分段锁底层实现源码剖析

    JAVA并发专题《金丹篇》

    21.AQS是个啥?为啥说它是JAVA并发工具基础框架?

    22.基于AQS的互斥锁底层源码深度剖析

    23.基于AQS的共享锁底层源码深度剖析

    24.ReentrantLock是怎么基于AQS实现独占锁的?

    25.ReentrantLock的Condition机制底层源码剖析

    26.CountDownLatch 门栓底层源码和实现机制深度剖析

    27.CyclicBarrier 栅栏底层源码和实现机制深度剖析

    28.Semaphore 信号量底层源码和实现机深度剖析

    29.ReentrantReadWriteLock 读写锁怎么表示?

  • ReentrantReadWriteLock 读写锁底层源码和机制深度剖析
  • JAVA并发专题《元神篇》并发数据结构篇

    31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能?

    32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能?

    33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能?

    34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现?

    35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样?

    36.DelayQueue 底层源码剖析,延时队列怎么实现?

    37.SynchronousQueue底层原理解析

    JAVA并发专题《飞升篇》线程池底层深度剖析

  • 什么是线程池?看看JDK提供了哪些默认的线程池?底层竟然都是基于ThreadPoolExecutor的?
  • 39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思?

    40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的?

  • ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?

  • ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?

  • ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?

  • ThreadPoolExecutor shutdown、shutdownNow内部核心流程

  • 再回头看看为啥不推荐Executors提供几种线程池?

  • ThreadPoolExecutor线程池篇总结

  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论