Java的锁机制

一.Java对象头

Synchronized用的锁是存储在Java对象头的,所以理解Java对象头的存储结构和存储数据的类型有助于对锁的理解;

Java对象头中主要存储三类数据:

  • 第一类叫做MarkWord,主要存储对象的hashcode,分代年龄,锁信息等运行数据;
  • 第二类是Class Pointer,指向方法区中该class的对象,JVM通过此字段来判断当前对象是哪个类的实例;
  • 第三类,数组的长度,就是如果当前对象是数组的话才会有。

三类中,我们这里重点关注第一类MarkWord,是我们理解锁的核心

|------|-------------|--------|------------|----------|
| 锁状态 | 25bit | 4bit | 1bit是否为偏向锁 | 2bit锁标志位 |
| 无锁状态 | 对象的hashcode | 对象分代年龄 | 0 | 01 |

程序运行期间,MarkWord存储的信息会随着锁标志位的变化而变化,可能会变化为以下四种状态之一;

锁状态 25bit 4bit 偏向锁标志(1bit) 锁标志位(2bit)
偏向锁 23位:线程ID 2位:Epoch 1 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10
GC标记 11

会发现无锁、偏向锁的"锁标志位"是一样的,即都是01,这是因为无锁、偏向锁是靠字段"是否是偏向锁"来区分的,0代表没有启用偏向锁,1代表启用偏向锁

二.锁的升级与对比

那么我们现在知道了,锁有四种状态,分别是:无锁,偏向锁,轻量级锁,重量级锁。这几种锁的状态会随着并发竞争的情况逐渐升级,锁只能升级不能降级(也就是说轻量级锁不能变成偏向锁)。

2.1 偏向锁

在大多实际环境下,锁不仅不存在多线程竟争,而且总是由同一个线程多次获取,那么在同一个线程反复获取所释放锁中,其中并没有锁的竟争,那么这样看上去,多次的获取锁和释放锁带来了很多不必要的性能开销和上下文切换。所以引入了偏向锁来处理这种情况。

通过你会发现无锁、偏向锁的"锁标志位"是一样的,即都是01,这是因为无锁、偏向锁是靠字段"是否是偏向锁"来区分的,0代表没有启用偏向锁,1代表启用偏向锁,可以通过JVM参数(XX:UseBiasedLocking=true 默认)控制。并且启动偏向锁还有延迟(默认4秒),可以通过JVM参数(XX:BiasedLockingStartupDelay=0)来关闭延迟.

**偏向锁加锁:**当一个线程A访问同步块并获取锁时,会在对象头存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程A的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试-下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。如果没有设置,则使用CAS竞争锁(即轻量级锁)

例子:当大呆需要上WC时,只有它自已要上WC,此时并没有其它的人需要上。WC,那么这时这个WC可以直接给大呆使用,并且大呆把可以标识自已身份的ID贴到门上,表示此时大呆占用了这个WC。

偏向锁撤销:还是用上面这个图来解释,此时当前的WC被大呆所占用,这时二呆来了也要使用WC。这时先等大呆解决完(执行完这次任务),大呆和二呆就要通过CAS的方式来抢占WC。因为此时锁的状态是偏向锁的状态,二呆来了也要使用WC(这时有两个人同时要使用WC,这时就要将偏向锁升级成轻量级锁),在升级轻量锁之前首先需要将WC上的标识大呆身份的ID撕下来(这一步叫做偏向锁的撤销)。

2.2 轻量级锁

上面锁被撤销后,升级为了轻量级锁,轻量级锁状态下两个人需要通过过自旋+CAS的方式两个人来抢锁。当其中一个线程抢锁成功后,会将LR贴到WC的门上,表示WC当前被某个线程占用,然后另一个没有抢到锁的线程就一直自旋获取锁。

  • 自旋的意思是占用CPU来反复尝试获取锁,直到获取成功
  • LR是Lock Record锁记录

LR的锁记录中存储的是对象的MarkWord的备份,即拷贝进入的,而++两个线程竞争的过程就是通过CAS的方式将对象本来的MarkWord位置存储的信息替换为指向自己LR记录的指针。谁替换成功了,谁就获得了锁++ ,例如A成功了。那没有获取到锁的线程B,就再自旋一段时间(自旋的原因是因为B认为A很快就能执行完,我就在门口等一下,也就是B认为竞争没有那么激烈)。当自旋-段时间后,如果还没有获得锁,那B就只能将锁修改为重量级锁了,然后所有竞争锁的线程进入阻塞状态,等待A执行完之后唤醒。

2.3 重量级锁

重量级锁,线程加锁失败会进入阻塞状态,等待前驱获得线程的锁执行完之后唤醒。

总结:

锁类型 优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,提高程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较慢

注意,偏向锁和重量级锁并没有使用monitor

相关推荐
苡~1 小时前
【openclaw+claude】手机+OpenClaw+Claude实现远程AI编程系列大纲
java·前端·人工智能·智能手机·ai编程·claude api
毕设源码-赖学姐1 小时前
【开题答辩全过程】以 基于java电脑售后服务管理系统设计为例,包含答辩的问题和答案
java·开发语言
我是秦始皇v我5001 小时前
CSDN:Java开发者的成长沃土
java
SoulruiA1 小时前
超容易理解+模版套路解决LeetCode 前序+中序、中序+后序、前序+后序遍历构造树问题
java·算法·力扣
蜗牛^^O^2 小时前
如何负责一个系统的稳定性
java
一只叫煤球的猫2 小时前
别再把 Lambda 当匿名类:这 9 类坑你一定踩过
java·后端·面试
知识即是力量ol2 小时前
微服务架构:从入门到进阶完全指南
java·spring cloud·微服务·nacos·架构·gateway·feign
Javatutouhouduan2 小时前
RocketMQ是怎么保存偏移量的?
java·消息队列·rocketmq·java面试·消息中间件·后端开发·java程序员
天若有情6732 小时前
IoC不止Spring!求同vs存异,两种反向IoC的核心逻辑
java·c++·后端·算法·spring·架构·ioc