小陈:牛逼的老王,快来了,我的笔记本已经准备好了,开讲了......
老王:哈哈,好,搞起来......
老王:我们这一章节接着上一章的内容继续讲下去,本章讲解的是synchronized是怎么通过monitor进行重量级加锁?
老王:在讲synchronized是怎么通过monitor进行重量级加锁之前,我们先回顾一下上一章的那个Mark Word用途的表格:
当Mark Word的最后两位的锁标志位是10的时候,Mark Word这哥们说自己处于重量级锁的模式,重量级加锁不是它的责任,是monitor的责任,它作为mark word记录的数据是monitor的地址,让我们自己去找monitor去进行加锁。
老王:小陈啊,上一章说的对象头和Mark Word的知识点还记得吧,不记得要回去再回顾下哦。
小陈:哈哈,哪能啊,老王,上一章的知识点已经完全在我脑子里了。
上一章说了对象的基本结构,以及对象头中的Mark Word的含义,其中非常重要的就是当Mark Word中最后两位的锁标志位是10的时候,Mark Word的前面是monitor监视器的地址,我现在就给你画出来对象头、Mark Word 和 monitor之间的关系图:
老王:哈哈,就知道小陈你聪明,啥都记住了,那我也就不啰嗦了......
老王:那我下面就直接说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有哪些属性,怎么通过这些属性加锁的还是完全不懂啊......
老王:没关系,慢慢来;我首先给你解释一波有哪些关键的属性,然后跟你说怎么通过这些属性加锁的。
小陈:好滴,老王就是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,也就是没线程加锁
(2)然后呢,这个时候线程A、线程B来竞争加锁了,如下图所示:
(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了?
老王:没错,就是这样的,释放锁的过程就是这么简单......
小陈:加锁和释放锁我看懂了,那monitor的其它属性是用来干啥的?
老王:我们慢慢来,接着上面的_count和_owner属性,我们接下来将的是 _spinFreq、_spinclock、_entrylist这几个东西:
上面说过 _spinFreq是等待锁期间自旋的次数、 _spinclock是自旋的周期也就是每次自旋多久时间、 _entrylist这个就是自旋次数用完了还没获取锁,只能放到 _entrylist等待队列挂起了。
让我们继续接着图来讲:
(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尝试几次之后就能获取成功了
(2)但是如果线程A拉肚子了,这家伙在里面蹲了一个多小时....,线程B尝试了10次之后,发现坑还是没有空的。 这个时候线程B发现自己还有好多代码没写,害~,不等了,先释放CPU去写写代码,待会再来看看......
小陈:牛逼啊,老王,你这么一说我全懂了。你上班是不是经常去...,所以才这么深的体会......
老王:滚......
小陈:嘿嘿,我好想发现了什么......
老王:打住打住...,看看现在时间九点半了,给你讲完后面的_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();
}
下面画个图来说一下:
(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并发专题《结丹篇》
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 读写锁怎么表示?
JAVA并发专题《元神篇》并发数据结构篇
31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能?
32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能?
33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能?
34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现?
35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样?
36.DelayQueue 底层源码剖析,延时队列怎么实现?
37.SynchronousQueue底层原理解析
JAVA并发专题《飞升篇》线程池底层深度剖析
39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思?
40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的?
ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?
ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?
ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?
ThreadPoolExecutor shutdown、shutdownNow内部核心流程
再回头看看为啥不推荐Executors提供几种线程池?
ThreadPoolExecutor线程池篇总结