此文是Java对象结构及内置锁的知识梳理总结,如有错漏欢迎在评论区指正;愿我,也愿在这个内卷时代努力提升自己的朋友们,坚持不懈,静待花开。
目标
- Java对象结构是什么样的
- Java内置锁工作的大体流程是怎样的
- 偏向锁的原理是什么,偏向锁何时撤销,偏向锁的升级流程是怎么样的
- 轻量级锁的原理是什么,锁升级的流程是什么
- 重量级锁的原理是什么
Java对象结构
java对象在内存中主要分为三个部分:对象头、对象体、对齐填充。对象体主要是对象的属性值数据,对象头又是由这些部分组成:Mark word、Class Point,若该对象是数组,对象头中还会有数组长度信息;其中Class Point存储的是Class对象的地址,Mark word是我们今天要了解的重点内容,它主要是存储:GC标志位、哈希码、锁状态等。 JVM中内置锁的状态有4种:无锁、偏向锁、轻量级锁和重量级锁。 不同锁状态下,32位JVM中Mark word结构如下:
Java内置锁的流程
Java内置锁有4种状态,任何一个Java对象都能作为对象锁。
无锁
对象刚创建还没有线程申请锁时,内置锁是无锁状态,此时偏向锁标志位为0,锁状态为01.
偏向锁
当有一个线程申请访问锁,发现锁状态为可偏向的状态,则尝试通过cas将偏向线程id指向当前线程,如果抢锁成功则获取偏向锁,如果失败则尝试进行锁升级;
在竞争偏向锁时,JVM会检查锁对象的偏向标志位,偏向标志位为0且偏向线程id为空,表明此时还没有线程来抢锁,直接获取锁成功,否则,JVM再检查持有锁的偏向线程是否存活,若偏向线程已死亡或JVM遍历偏向线程的栈帧判定该偏向线程已经不会再持有该偏向锁,则当前线程将抢锁成功,否则进行锁升级。
偏向锁的优势:偏向锁不会主动撤销,在只有一个线程使用该锁的时候,偏向线程访问锁不需要进行任何同步操作,只需要判断当前偏向线程是不是自己,如果是就可以直接进入同步代码块;但是JVM有一个偏向锁批量撤销的机制,当该类的锁对象频繁切换偏向线程,则会批量撤销该类的偏向锁;另外JVM对偏向锁是延迟开启的,可以通过JVM参数禁用偏向锁。
偏向锁的撤销:当多个线程竞争偏向锁或者调用锁对象的hashCode()方法时,触发偏向锁撤销。偏向锁的撤销开销比较大,撤销时需暂停偏向线程(stop the world),检查偏向线程是否存在锁记录,如果存在则清空锁记录,且将偏向线程id清空,偏向锁升级成轻量级锁,唤醒当前线程
轻量级锁
升级条件:有两个线程同时竞争偏向锁时,偏向锁升级为轻量级锁
升级过程:暂停偏向线程A,在线程A的栈帧创建锁记录,锁记录包含2个部分,一部分为锁对象的原始mark word(Displace mark word),一部分是锁对象指针;复制锁对象的mark word到锁记录中,将锁对象指针指向锁对象,通过cas将锁对象的锁记录指针指向当前线程的锁记录,如果cas抢锁成功,则成功获取锁,进入同步块执行,否则进入自旋(即循环不停尝试通过cas获取锁)
轻量级锁分类:分为普通自旋锁和自适应自旋锁;普通自旋锁默认最大自旋10次,如果失败则说明锁竞争比较激烈,进行锁升级;自适应自旋锁基于抢锁线程上次在该锁上是否抢锁成功,如果成功则允许自旋更长时间,如果很少获得过成功,则JVM将减少甚至取消自旋过程。
优势:轻量级锁通过cas实现,是一种非阻塞、乐观锁,cas是操作系统的原子操作,执行完记录返回,轻量级锁因此不会让线程陷入阻塞,而是陷入空循环,如果线程一直陷入这种空循环,也会导致cpu空转,浪费cpu资源。
重量级锁
升级条件:轻量级锁自旋失败时,将触发锁升级为重量级锁
升级过程:
-
JVM 在堆中为锁对象申请一个
ObjectMonitor对象(这是重量级锁的核心数据结构,C++实现,内部有复杂的等待队列、条件变量等)。这个对象才是真正的“重量级锁”。 -
设置锁状态:
- JVM 将锁对象的
Mark Word更新为指向这个ObjectMonitor对象的指针。此时,锁的标志位变为10,表示进入重量级锁状态。
- JVM 将锁对象的
-
阻塞竞争者线程:
- 还在自旋等待的线程B(以及后续所有来竞争的线程),当它们看到锁标志位是
10时,就知道这个锁已经膨胀了。 - 这些线程会停止自旋,并执行操作系统级别的互斥操作,将自己挂起,进入该
ObjectMonitor的等待队列(_cxq 或 _EntryList)中,等待被操作系统调度。这个挂起操作需要从用户态切换到内核态,成本非常高。
- 还在自旋等待的线程B(以及后续所有来竞争的线程),当它们看到锁标志位是
-
处理原持有者线程:
- 那么,现在正持有轻量级锁的线程A怎么办呢?
- 当线程A执行完同步代码块,准备释放锁时,它会使用CAS操作,试图将Displaced Mark Word写回到锁对象的对象头。
- 但这个CAS操作会失败! 因为对象头的
Mark Word已经在膨胀过程中被修改为指向ObjectMonitor的指针了。 - CAS失败会让线程A知道,锁已经膨胀了。
- 于是,线程A会进入重量级锁的释放流程:它需要唤醒在
ObjectMonitor中等待的线程(比如线程B),并将锁的所有权移交。
整个锁升级的过程,对于持有锁的线程来说是无感知的,它仍然认为自己持有的是轻量级锁,只有在释放锁时通过cas将锁记录的Displaced mark word写回锁对象失败时,第一次感知到锁升级,于是进行重量级锁的释放,唤醒ObjectMonitor对象的阻塞队列中的第一个线程
重量级锁是一个不公平锁,不公平的原因在于,当一个线程进行抢锁是,不是马上进入监视器对象的阻塞队列中,而是先通过cas进行一次快速抢锁将监视器中的owner改成自己,这样做对于已经进入监视器锁队列中的线程是不公平的。
重量级锁的开销:重量级锁中,抢锁失败的线程会通过系统调用进入阻塞状态,线程释放锁时需要将阻塞队列中的第一个线程唤醒,由用户态陷入内核态,用户态到内核态之间的切换比较耗费时间,重量级锁是通过linux内核的互斥锁实现的。