当前位置: 首页 > ops >正文

JAVA 锁—— synchronized

在这里插入图片描述

32 位机器上java对象头中,markWord 示意图如上所示,64 位机器扩展前面标识位数,如 hashcode(25 -> 31),线程ID(23 -> 54)

如果启用了偏向锁:

  • synchronized添加偏向锁:只有1个线程加锁的情况下,此时一定没有竞争
  • synchronized添加轻量级锁:超过1个线程,且交替加锁,此时没有竞争
  • synchronized添加重量级锁:超过1个线程,同时加锁,或加锁时,其它线程没释放锁,此时发生了竞争

一、基本概念

注:下面代码大部分基于 JDK 8 分析,后续有 JDK23 的分析。

1、偏向锁

为方便观察 Java 内存对象,我们使用JOL工具,详见Java对象的内存分布(一)。

1.1、代码

	/***  程序运行前最好等待5秒,开启偏向锁。*  因为 jvm 启动也有加锁需求,防止 jvm 启动时受偏向锁影响,比如锁升级带来消耗,*  故而 jvm 完全启动后(大约4s,通过参数 -XX:BiasedLockingStartupDelay=0 进行调整),才会应用偏向锁。*/public static  void main(String[] args) throws Exception{Thread.sleep(5000l); Object lock = new Object();System.out.println("---第一次----");System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());synchronized (lock) {System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}System.out.println("加锁后:" + ClassLayout.parseInstance(lock).toPrintable());System.out.println("---第二次----");System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());synchronized (lock) {System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}System.out.println("加锁后:" + ClassLayout.parseInstance(lock).toPrintable());}

1.2 运行结果

在这里插入图片描述

  • 大端模式:高位字节存放于内存的低地址端,低位字节存放于内存的高地址端,便于人类阅读。
  • 小端模式:低位字节存放于内存的低地址端,高位字节存放于内存的高地址端,便于机器处理。
存储模式示例(0x12345678)
大端(Big-Endian)0x12 0x34 0x56 0x78
小端(Little-Endian)0x78 0x56 0x34 0x12

1.3、注意事项

Object.hashCode()方法和System.identityHashCode()会让对象不能使用偏向锁,所以如果想使用偏向锁,那就最好重写hashCode方法。

  • 无锁和偏向锁占用相同位置,不像轻量级锁和重量级锁可以将原位置信息拷贝到其它地方进行备份,所以当对象已经存储了hashcode之后,加锁时会跳过偏向锁。
  • 偏向锁不会释放,即解锁后,锁对象头MarkWord不变。
  • JDK [6, 15),偏向锁默认开启,从 JDK 15 开始,默认关闭,可以通过 -XX:+UseBiasedLocking 开启,从JDK 18 开始彻底移除偏向锁。

2、轻量级锁

2.1、代码

	/*** 开启偏向锁后,2个线程交替加锁,偏向锁升级为轻量级锁。*/public static  void main(String[] args) throws Exception{Thread.sleep(5000l); // 等待5秒,开启偏向锁Object lock = new Object();System.out.println("---第一次----");System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());synchronized (lock) {System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}System.out.println("加锁后:" + ClassLayout.parseInstance(lock).toPrintable());System.out.println("---第二次----");Thread thread = new Thread(() -> {synchronized (lock) {System.out.println("线程2:");System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}System.out.println("线程2:");System.out.println("加锁后:" + ClassLayout.parseInstance(lock).toPrintable());});thread.start();}

2.2、运行结果

在这里插入图片描述

2.3、锁细节

轻量级锁加锁时,markWord中会存储指向栈中锁记录的指针,栈中锁记录存储的就是未加锁时原来的markWord,解锁时方便还原回去。

假如markWord中存储了hashcode,使用时先访问markWord发现加了轻量级锁,顺着栈指针找到栈中锁记录,即可找到hashcode


3、重量级锁

3.1、代码

    public static  void main(String[] args) throws Exception{Thread.sleep(5000l); // 等待5秒,开启偏向锁Object lock = new Object();System.out.println("---第一次----");System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());synchronized (lock) {System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());System.out.println("---第二次----");Thread thread2 = new Thread(() -> {synchronized (lock) {System.out.println("线程2:[" + Thread.currentThread() +  "] 加锁中:" + ClassLayout.parseInstance(lock).toPrintable());try {Thread.sleep(5000L); // 这里持锁5s,确保线程3加锁发生竞争;} catch (java.lang.Exception e) {}}});Thread thread3 = new Thread(() -> {synchronized (lock) {System.out.println("线程3:[" + Thread.currentThread() +  "] 加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}});thread2.start();thread3.start();Thread.sleep(8000L); // 等待8s,确保线程锁释放;System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());}

3.2、运行结果

在这里插入图片描述

3.3、Monitor

  • 偏向锁、轻量级锁只要发生竞争,就会升级为重量级锁,注意,这里一步到位,不会自旋。

  • 升级为重量级锁后,其它线程自旋多次失败后,会进入 cxq 列表(相当于栈)中自旋,自旋达到阈值后,仍未获取锁,则进入阻塞状态。

  • 重量级锁加锁时,markWord中会存储指向 Monitor 的指针,Monitor 会存储未加锁时原来的markWord,解锁后还原回去。【JDK8实验如此,JDK23实验不一样】

    假如markWord中存储了hashcode,使用时先访问markWord发现加了重量级锁,顺着重量级锁指针找到Monitor,即可找到hashcode

截止目前为止,上述代码均在 JDK 8上讨论;下面讨论 JDK23

  • 重量级锁加锁时,markWord中会存储指向 Monitor 的指针,Monitor 会存储未加锁时原来的markWord,解锁时markWord不变。【JDK23实验,代码不变,运行结果如下图所示】
    在这里插入图片描述
ObjectMonitor细节如下:

在这里插入图片描述

由上图可知, wait()notify()notifyAll() 只能在重量级锁中调用,换言之,在偏向锁和轻量级锁中调用这3个方法时,会升级为重量级锁。

CAS详见CAS基础概念。


3.4、重量级锁会降级为轻量级锁吗?

    public static  void main(String[] args) throws Exception{Thread.sleep(5000l); // 等待5秒,开启偏向锁Object lock = new Object();System.out.println("---第一次----");System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());synchronized (lock) {System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());System.out.println("---第二次----");Thread thread2 = new Thread(() -> {synchronized (lock) {System.out.println("线程2:[" + Thread.currentThread() +  "] 加锁中:" + ClassLayout.parseInstance(lock).toPrintable());try {Thread.sleep(5000L); // 这里持锁5s,确保线程3加锁发生竞争;} catch (java.lang.Exception e) {}}});Thread thread3 = new Thread(() -> {synchronized (lock) {System.out.println("线程3:[" + Thread.currentThread() +  "] 加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}});thread2.start();thread3.start();Thread.sleep(8000L); // 等待8s,确保线程锁释放;System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());Thread thread4 = new Thread(() -> {synchronized (lock) {System.out.println("线程4:[" + Thread.currentThread() +  "] 加锁中:" + ClassLayout.parseInstance(lock).toPrintable());}});thread4.start();

【JDK 8 重量级锁会降级为轻量级锁】

在这里插入图片描述

【JDK 23 重量级锁不会降级为轻量级锁】

在这里插入图片描述


二、补充知识

1、hashcode对锁的影响

1.1、偏向锁状态中,首次调用锁的hashcode后,偏向锁会直接升级为重量级锁。

    public static  void main(String[] args) throws Exception{Thread.sleep(5000l); // 等待5秒,开启偏向锁Object lock = new Object();System.out.println("---第一次----");System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());synchronized (lock) {System.out.println("加锁中-未调用hashcode :" + ClassLayout.parseInstance(lock).toPrintable());lock.hashCode();System.out.println("加锁中-已调用hashcode:" + ClassLayout.parseInstance(lock).toPrintable());}System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());}

在这里插入图片描述

1.2、轻量级锁状态中,首次调用锁的hashcode后,锁升级为重量级锁?

    public static  void main(String[] args) throws Exception{Object lock = new Object();System.out.println("---第一次----");System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());synchronized (lock) {System.out.println("加锁中-未调用hashcode :" + ClassLayout.parseInstance(lock).toPrintable());lock.hashCode();System.out.println("加锁中-已调用hashcode:" + ClassLayout.parseInstance(lock).toPrintable());}System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());}

【JDK 8 升级重量级锁】

在这里插入图片描述
【JDK 23 维持轻量级锁】
在这里插入图片描述


唯一不变就是变化,JDK也在不断的演进,昨天还正确的观点,今天就错误了;今天错误的观点也可能明天就正确了。

所以看到任何观点,都要保持怀疑态度啊,每个人都有自己的观点,切忌坐井观天,故步自封,一定要跳出去,用发展的视角看问题。

http://www.xdnf.cn/news/5443.html

相关文章:

  • linux 三剑客命令学习
  • C++基本知识 —— 缺省参数·函数重载·引用
  • 蓝桥杯14届国赛 合并数列
  • 【Python 算法零基础 2.模拟 ⑤ 基于栈和队列】
  • 【JEECG 组件扩展】JSwitch开关组件扩展单个多选框样式
  • 【AI智能推荐系统】第八篇:可解释AI在推荐系统中的实践与价值
  • 深度优先与广度优先:如何用算法思维优化学习策略?
  • 250510-Linux离线配置N8N环境+屏蔽外网请求
  • python使用AES进行加密和解密
  • JavaSE基础
  • python: 为项目创建单独的虚拟环境步骤
  • QSS样式表的选择器
  • 蓝牙RFCOMM协议概述
  • 第二十一节:图像金字塔-高斯金字塔
  • TTS-Web-Vue系列:移动端侧边栏与响应式布局深度优化
  • OSCP备战-kioptrixvm3详细解法
  • [Java实战]Spring Boot 中Starter机制与自定义Starter实战(九)
  • Linux为啥会重新设置中断请求号与中断向量号之间的关系?
  • vector--OJ1
  • 【外网下载Jar包】
  • AI技术驱动SEO关键词智能布局
  • go程序编译成动态库,使用c进行调用
  • linux--------------Ext系列⽂件系统(下)
  • QOwnNotes:功能强大的跨平台笔记应用程序
  • FreeRTOS静态任务的创建、删除和软件定时器的开启(尚硅谷学习笔记)
  • 监控易一体化运维:任务计划管理的关键作用
  • 王道计算机网络知识点总结
  • 动态路由实现原理及前端控制与后端控制的核心差异
  • Linux:43线程封装与互斥lesson31
  • 前端Web开发HTML5+CSS3+移动web(基础-flex)