目录标题
前置知识:JAVA对象结构
假如有一个Test类如下:
public class Test {
private int a = 8;
private ArrayList list = new ArrayList();
}
那么Test对象结构如下图所示
一个java实例对象由三部分组成,分别是:
对象头
:对象头由Mark Word 和 一个指向一个类对象的指针组成。
实例数据
:存放这个实例的一些属性信息,比如有的属性是基本类型,那就直接存储值;如果是对象类型,存放的就是一个指向对象的内存地址。
对其填充
:主要是补齐作用,JVM对象的大小比如是8字节的整数倍,如果 (对象头 + 实例变量 )不是8的整数倍,则通过对齐填充来补齐。
比如 (对象头 + 实例变量) 部分的大小是20个字节,不是8的整数倍,那么对齐填充这里就会补上4个字节,使得(对象头 + 实例变量 + 对其填充)= 24字节,为8的整数倍。
举例
比如说Test test = new Test(),我就来再画个图细说一下test对象的结构:
如上图所示:
对象头:由Mark Word 和一个指向类对象的指针组成。
实例变量:记录这个对象哪些属性,如果是基本类型,直接就记录值了;如果是对象类型,则记录一下对象在堆内存的地址,方便以后找到它来使用。
对其填充:这块东西就是为了对象的大小满足8的整数倍,进行补齐的
Mark Word详解
Mark Word是一个32位的数据结构,存在于对象头里面,大致结构如下:
无锁
偏向锁标志是0,锁标志位是01,也就是最后3位是001的时候,表示无锁模式。记录的数据是对象的hashcode 和 GC的年龄
偏向锁
偏向锁标志是1,锁标志是01,也就是最后三位是101的时候,处于偏向锁模式,记录的数据是获取偏向锁的线程ID、Epoch、对象GC年龄
轻量级锁
锁标志位是00的时候,表示处于轻量级锁模式。把锁记录放在加锁的线程的虚拟机栈空间中,所以这种情况下,锁记录在哪个线程虚拟机栈中,就表示所在线程就获取到了锁。
作为Mark Word记录的数据就是就指向那个锁记录地址就好了,这个锁记录地址在哪个线程中,就表示哪个线程获取到了轻量级锁。
重量级锁
锁标志位是10的时候,表示处于重量级锁模式,由于使用重量级加锁是monitor的职责,作为Mark Word 记录的数据是monitor的地址,根据地址找monitor加锁就好了。
synchronized是怎么通过monitor进行重量级加锁
对象头、Mark Word 和 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对象的关键属性
_owner
:指向加锁的线程,比如线程A获取锁成功了,则_owner = 线程A;当_owner = null的时候表示没线程加锁
_recursions
:线程的重入次数
_cxq
:多线程竞争锁时的单向链表(先进后出;可自旋)
_entrylist
: 阻塞队列,来自cxq(调unlock时,_cxq 队列中有资格成为候选资源的线程会被移动到该队列中)或者waitSet(调notify时)
_waitset
:等待队列,调wait的线程在这里。只有有别的线程调用notify将它唤醒。
_spinFreq
:获取锁失败前自旋的次数【就是线程获取锁失败之后,每隔一段时间都会重试去争抢一次,如果超过了这个自旋次数抢不到,那线程只能沉睡了。】
_spinClock
:上面说获取锁失败每隔一段时间都会重试一次,这个属性就是自旋间隔的时间周期,比如50ms,那么就是每隔50ms就尝试一次获取锁。
三个重要的队列
cxq(竞争列表):单向链表,先进后出
cxq是一个单向链表。被挂起线程等待重新竞争锁的链表,,monitor 通过CAS将包装成ObjectWaiter写入到列表的头部。为了避免插入和取出元素的竞争,所以Owner会从列表尾部取元素。所以这个东西可以理解为一上来竞争没拿到锁的在这里临时待一会(1级缓存)。
EntryList(锁候选者列表)
EntryList是一个双向链表。当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程为OnDeck线程,其他线程就待在里面。所以这个东西可以认为是二次竞争锁还没拿到的(里面有一个马上就会拿到)。(2级缓存)
是队列用来获取锁的缓冲区,用来将cxq和waitSet中的数据 移动到entryList进行排队。这个统一获取锁的入口。一般是cxq 或者waitSet数据复制过来进行统一排队。
EntryList跟cxq的区别
在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。而EntryList中的线程都是被挂起的线程。
WaitList
WatiList是Owner线程地调用wait()方法后进入的线程。进入WaitList中的线程在notify()/notifyAll()调用后会被加入到EntryList。
加锁过程
成功获取锁流程
通过CAS尝试将_owner标记为当前线程;
如果线程第一次进去monitor,则将_owner标记为当前线程,_recursions 标记为1,表示当前线程获得锁;
如果之前的_owner指向当前线程,说明线程再次进入monitor(重入锁),执行_recursions ++。
没有获取到锁流程
通过自旋尝试获取锁(指定时间间隔,次数自旋获取锁),如果获取到直接返回。满满的求生生欲望,避免自己加入到等待。
如果还是没有获取到锁,调用EnterI方法让自己加入到cxq等待队列【当前线程被封装成 ObjectWaiter 对象 node,通过 CAS 把 node 节点 push 到_cxq列表中,因为同一时刻可能有多个线程把自己的 node 节点 push 到_cxq列表中】,通过 park 将当前线程挂起等待被唤醒。
释放锁
根据QMode的不同,有不同的处理方式:
QMode=1 的时候 这个是默认策略 优先从_EntryList 中获取 如果_EntryList为空的情况, 将倒置cxq队列,加入到EntryList中,然后从EntryList中唤醒线程
QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,后面的代码不会执行了;
QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;
QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;
monitor的wait和notify
如果调用了Object中的wait方法,此时释放锁,将_count恢复为0,将_owner指向 null,然后将自己加入到waitset集合中,等待别人调用notify或者notifyAll将其中waitset的线程唤醒。
notify方法并不一定能够立刻唤醒调用Wait的线程。
线程的notify/nofityAll方法在jvm源码中并没有唤醒线程,而是从waitSet链表取出一个节点进行挪动,等到真正出了synchronized代码块时,根据QMode策略进行唤醒操作。
只有再次调用notify/notifyAll 方法 才能将waitSet队列中的头节点或者全部节点加入到EntryList中,然后在由线程释放锁之后进行唤醒。至于什么时候能够抢到锁,需要看waitSet中节点是如何移动的。
Policy=0:将ObjectWaiter放入到enteylist队列的排头位置
Policy=1:放入到entrylist队列末尾位置
Policy=2(默认):判断entrylist是否为空,为空就放入到entrylist中,否则放入到cxq队列的排头位置
Policy=3:判断cxq是否为空,如果为空,直接放入头部,否则放入cxq队列末尾位置
notify和notifyAll有啥区别
notify就是从waitset中随机挑一个线程来唤醒,只唤醒一个。notifyAll这方法就是将waitset中所有等着的线程全部唤醒。
场景
synchronized(this) {
if (某个条件) {
wait();
}
}
synchronized(this) {
// 某些业务逻辑
......
notify();
}
- 线程A先获取到了锁。
- 线程A发现条件不满足,先释放锁,等条件满足了,别人再唤醒我,于是释放了锁
- 线程B可以加锁了,执行了一些业务逻辑,然后去调用notify方法唤醒线程A
- 线程A醒来之后,还是要再去去竞争锁的,也就是醒来之后还要竞争将_count修改为1,竞争_owner指向自己,毕竟它还在synchronized代码块内部嘛,只有获取锁之后才能执行synchronized代码块的代码。所以只有它再次获取到锁了之后,才会执行代码块内部的逻辑
为什么wait要结合synchronized一起使用
因为waitset集合是monitor对象的一个属性,所以调用之前必须要获取到monitor对象的操作权限,也就是获取到锁,notify要操作waitset也是一样。所以wait和notify方法之后在获取了锁之后才能调用的,进入synchronized获取锁了之后才能执行。
wait() 和 Thread.sleep()的区别
wait()会释放锁,而Thread.sleep()不释放锁
synchronized(this) {
// 这个时候线程释放锁,然后将自己放入monitor的waitset队列,
// 等待别人调用notify/notifyAll将唤醒
wait();
}
synchronized(this) {
// 这种情况不释放锁,就是睡个500ms然后醒来持有锁继续干活
Thread.sleep(500);
}
原文:
synchronized底层之monitor、对象头、Mark Word?
synchronized底层是怎么通过monitor进行加锁的?
monitor机制
加锁释放锁流程
最后一个图来源
https://blog.csdn.net/sky_ccy/article/details/124124662