java的🔒(未完待续)
还没写完呢
在这之前先补充一些知识吧,方便后续相关知识的理解
一、内存中的对象长什么样子?
一张图说清楚吧。
堆中的java对象只有数据部分,对象中的数据含义,要结合方法区中的class才能明了。
堆中的对象结构包括三部分:对象头、实例数据、填充数据。这里主要是见一下对象头部分,对象头可以说是一个对象的身份证了。
对象头的构成如下:
1 | +------------------+------------------+------------------+------------------+ |
数组的对象头构成如下:
1 | |---------------------------------------------------------------------------------| |
- 普通对象:Mark Word + Klass Pointer 数组对象:额外包含4字节数组长度
MarkWord的结构如下:
简单先看到这里。这篇博客的主菜是锁机制。
好了,正式开始介绍锁机制。
二、锁机制。
现代计算机系统中,锁的实现本质上是硬件指令与软件逻辑的协同产物。在x86架构中,LOCK
前缀指令通过锁总线或缓存行来实现原子操作,而ARM架构的LDREX/STREX
指令对则采用独占监视器机制。这些硬件特性构成了Java锁机制的物理基础。
——-摘自网络,都是中文,组成一起感觉就有点看不懂了。😵😵😵
2.1锁的本质:解决三大并发问题
- 原子性:防止指令交错导致的中间状态(如i++的非原子操作)
- 有序性:通过内存屏障强制刷新缓存(volatile变量的写-读建立happens-before关系)
- 可见性:阻止指令重排序(单线程语义下的as-if-serial vs 多线程环境下的真实执行顺序)
2.2锁的分类
- 悲观锁:synchronized、Reentrant Lock
- 乐观锁:CAS、版本号控制。
三、慢慢剥开悲观锁!
3.1Monitor
在介绍悲观锁之前,先说一下Monitor,synchronized给对象上锁时,会把这个对象跟OS提供的一个Monitor相关联。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
注意:synchronized 必须是进入同一个对象的 monitor 才有上述的效果,不加 synchronized 的对象不会关联监视器,不遵从以上规则
你也可以看这个:
1 | ┌──────────────────┐ |
在 Hotspot 虚拟机中,Monitor 由 Object Monitor 实现
1 | ObjectMonitor() { |
- _owner:当前持有 Object Monitor 的线程,初始值为 null,表示没有线程持有锁。线程成功获取锁后,该值更新为线程 ID,释放锁后重置为 null。
- _count:记录当前线程获取锁的次数(可重入锁),每次成功加锁
_count + 1
,释放锁_count - 1
。 - _Wait Set:等待队列,调用
wait()
方法后,线程会释放锁,并加入 _Wait Set,进入 WAITING 状态,等待notify()
唤醒。 - _cxq:阻塞队列,用于存放刚进入 Monitor 的线程(还未进入 _Entry List)。
- _Entry List:竞争队列,所有等待获取锁的线程(BLOCKED 状态)会进入 _Entry List,等待锁释放后竞争执行权。
1 | +----------------------+ |
3.2 悲观锁☞synchronized
synchronized 依赖 JVM 内部的 Monitor 对象来实现线程同步。使用的时候不用手动去 lock 和 unlock,JVM 会自动加锁和解锁。
synchronized 加锁代码块时,JVM 会通过 monitorenter
、monitorexit
两个指令来实现同步
- monitorenter 表示线程正在尝试获取 lock 对象的 Monitor
- monitorexit表示线程执行完了同步代码块,正在释放锁
看一下代码
1 | static final Object lock = new Object(); |
对应的部分字节码对应如下:
注意:方法级别的 synchronized 不会在字节码指令中有所体现。
3.3synchronized的锁升级过程:
①偏向锁:JDK15之后,偏向锁被废弃了,那我这里就不多赘述了。
至于为什么被删除?问了下AI:偏向锁被废弃是因为它在现代多核 CPU 和高并发场景下性能提升不明显,反而增加了虚拟机的复杂性和维护成本。
②轻量级锁:如果一个对象虽然有多个线程要加锁,蛋加锁的时间是错开的(也就是没有竞争),那么可以室友轻量级锁来优化。存在竞争时呢,轻量级锁就会变成重量级锁。轻量级锁锁对于使用者时透明的,即语法依然是synchronized。
看下轻量锁的过程:
创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
如果 CAS 失败,有两种情况
**·**如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
**·**如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头,成功,则解锁成功。失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
③锁膨胀
如果尝试加锁轻量锁的过程中,CAS操作无法成功,这时就是有其它线程为此对象先加上了轻量锁,现在冲突了,需要进行锁膨胀,将轻量锁变为重量锁。
线程1加锁失败之后就会去为这个线程申请Monitor锁,该线程进入Monitor的EntryListBLOCKED,当线程0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,恢复失败的话