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

相关推荐
tuyanfei10 小时前
Spring 简介
java·后端·spring
遥遥晚风点点10 小时前
JAVA http请求报错:unable to find valid certification path to requested target
java·网络·网络协议·http
ZhengEnCi10 小时前
J0A-JPA持久化技术专栏链接目录
java·数据库
代码探秘者10 小时前
【大模型应用】2.RAG详细流程
java·开发语言·人工智能·后端·python
xieliyu.10 小时前
Java :类和对象(一)
java·开发语言
xuboyok210 小时前
Spring Boot管理用户数据
java·spring boot·后端
阳光下的米雪10 小时前
记一次pgsql中with as语法的使用以及with as介绍
java·数据库
qq_3677193010 小时前
Android MQTT开源库paho.mqtt.android+MQTTX软件使用记录
android·java·开源·android mqtt开源库·mqttx软件使用
6+h10 小时前
【java IO】字节流详解
java·开发语言·python
Mem0rin10 小时前
[Java面向对象]接口的声明和实现继承
java·开发语言