synchronized底层到底是怎么加锁的

前置知识: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();
}

在这里插入图片描述

  1. 线程A先获取到了锁。
  2. 线程A发现条件不满足,先释放锁,等条件满足了,别人再唤醒我,于是释放了锁
  3. 线程B可以加锁了,执行了一些业务逻辑,然后去调用notify方法唤醒线程A
  4. 线程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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值