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

相关推荐
Amarantine、沐风倩✨2 小时前
一次线上性能事故的处理复盘:从 SQL 到扩容的工程化思路
java·数据库·sql·oracle
tb_first2 小时前
万字超详细苍穹外卖学习笔记1
java·jvm·spring boot·笔记·学习·tomcat·mybatis
代码匠心2 小时前
从零开始学Flink:状态管理与容错机制
java·大数据·后端·flink·大数据处理
zhougl9962 小时前
Java内部类详解
java·开发语言
茶本无香2 小时前
设计模式之十二:模板方法模式Spring应用与Java示例详解
java·设计模式·模板方法模式
灯火不休ᝰ3 小时前
[kotlin] 从Java到Kotlin:掌握基础语法差异的跃迁指南
java·kotlin·安卓
KoiHeng3 小时前
Java的文件知识与IO操作
java·开发语言
czlczl200209253 小时前
Spring Data Redis
java·redis·spring
知识即是力量ol3 小时前
在客户端直接上传文件到OSS
java·后端·客户端·阿里云oss·客户端直传