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

Synchronized的实现原理:深入理解Java线程同步机制

前言

在多线程编程中,保证线程安全是一个至关重要的问题。Java提供了synchronized关键字来实现线程同步,它是Java中最基本、最常用的同步机制。本文将深入探讨synchronized的实现原理,帮助读者更好地理解其工作机制和使用场景。

1. synchronized的基本用法

在深入原理之前,我们先回顾一下 synchronized 的三种基本用法:

1.1 同步实例方法

public synchronized void method(){// 同步代码
}

1.2 同步静态方法

public static synchronized void staticMethod(){// 同步方法
}

1.3 同步代码块

public void method(){synchronized(this){// 同步代码}
}

或者使用类对象:

public void method(){synchronized(MyClass.class){// 同步代码}
}

2. synchronized 的实现原理

synchronized 实现原理主要基于对象头 Monitor(监视器)机制

2.1 对象头与Mark Word

在 Hotspot 虚拟机中,一个Java对象的存储结构,在内存中的存储布局分为 3块区域:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

对象头(Object Header)又包括两部分信息:

  • Mark Word:存储对象的哈希码、分代年龄、锁标志位等
  • Klass Pointer(32位):指向对象类型数据的指针(指向对象所属类的元数据(Class对象),JVM 通过该指针确定对象是哪个类的实例)

指针压缩(Compressed OOPs)

作用:64位JVM中,通过压缩列表将 Klass Pointer 从8字节压缩为4字节,减少内存占用。

开启方式:默认开启(-XX:+UseCompressedOops),堆内存超过32GB时自动关闭指针压缩,恢复为8字节。

Mark Word 在不同锁状态下的结构如下:

锁状态25bit4bit1bit(是否偏向锁)2bit(锁标志位)
无锁对象的 hashCode对象分代年龄001
偏向锁线程IDEpoch101
轻量级锁指向栈中锁记录的指针00
重量级锁指向 Monitor 的指针10
GC标记11

2.2 Monitor (监视器)机制

synchronized 的实现依赖于对象内部的监视器锁(Monitor)。每个Java对象都与一个Monitor相关联,当线程尝试获取对象的锁时,实际上是在尝试获取对象关联的 Monitor 的所有权。

Monitor的主要组成部分:

当一个线程尝试访问被 synchronized 保存的代码块时,其实相当于,线程通过 monitorenter 指令尝试获取 monitor 的所有权:

  • 获取 Monitor :线程会首先检查对象的 Mark Word 是否指向当前线程的锁记录(轻量级锁),或是否指向 Monitor 对象(重量级锁)。
  • 竞争锁:如果 Monitor 已被其他线程占用,则当前线程会被阻塞,进入 Entry List 队列等待。
  • 释放锁:持有锁的线程执行完同步代码块后,会释放 Monitor ,并唤醒 Entry List 中的等待线程重新竞争。

2.3 锁的升级过程

为了减少获得锁和释放锁带来的性能消耗,Java SE 1.6 引入了锁升级机制,锁的状态会随着不同的线程竞争情况,逐渐升级。

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

2.3.1 偏向锁

目的:在无竞争的情况下减少同步开销。

工作原理

  1. 当线程第一次访问同步块时,会在对象头和栈帧中记录偏向的线程ID
  2. 以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁
  3. 如果有其他线程尝试竞争锁,偏向模式宣告结束

2.3.2 轻量级锁

目的:在没有多线程竞争的前提下,减少传统重量级锁的性能消耗

加锁过程

  1. 在代码进入同步代码块时,如果同步对象没有被锁定,虚拟机将当前线程的栈帧各种建立一个锁记录(Lock Record)空间
  2. 将对象头的 Mark Word 复制到锁记录中(DisPlaced Mark Word
  3. 尝试使用CAS将对象头的 Mark Word 替换为指向锁记录的指针
  4. 如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁

解锁过程

  1. 使用CAS操作将 DisPlaced Mark Word 替换回对象头
  2. 如果成功,表示没有竞争发生;如果失败表示存在竞争,锁会膨胀为重量级锁

2.3.3 重量级锁

当轻量级锁竞争激烈时,会升级为重量级锁。此时,未获得锁的线程会被阻塞,等待操作系统调度,涉及到用户态到内核态的切换,性能开销较大

3. synchronized 的底层实现

3.1 字节码层面

从字节码角度看,synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令:

public void test(){synchronized(this){System.out.println("Hello World");}
}

对应的字节码:

0: aload_0
1: dup
2: astore_1
3: monitorenter      // 进入同步块
4: getstatic #2
7: ldc #3
9: invokevirtual #4
12: aload_1
13: monitorexit       // 正常退出同步块
14: goto 22
17: astore_2
18: aload_1
19: monitorexit       // 异常退出同步块
20: aload_2
21: athrow
22: return

可以看到,编译器会自动生成一个异常处理器,确保即使同步块中抛出异常,锁也能被正确释放。

对于同步方法,方法的访问标志中会设置 ACC_SYNCHRONIZED 标志,当方法调用时,调用指令会检查该标志。

3.2 内存语义

synchronized 具有以下内存语义:

  1. 进入代码块:清空工作内存中的变量副本,从主内存重新加载
  2. 退出同步块:将工作内存中的修改刷新到主内存

这保证了多线程环境下变量的可见性有序性

4. synchronized 的性能问题

  1. 锁粒度太大:同步范围覆盖过多无关代码,导致线程竞争加剧。
  2. 锁持有时间过长:同步块中包含耗时操作(如IO、网络请求)。
  3. 锁竞争激烈:多个线程频繁争抢同一把锁,导致上下文切换频繁。
  4. 锁升级频繁:大量竞争导致锁从偏向锁升级到重量级锁。
  5. 死锁:线程互相等待对方释放锁,导致系统停滞。

5. 优化建议

  1. 减少同步范围:尽量缩小同步代码块的范围;
  2. 降低锁粒度:将一个大锁拆分为多个小锁;
  3. 避免嵌套锁:尽量避免在同步块内调用其他同步方法;
  4. 使用并发容器:优先考虑使用 ConcurrentHashMap 等并发容器;
  5. 考虑读写锁:读多写少的场景考虑使用 ReadWriteLock

6. 总结

synchronized 是Java中实现线程同步的重要机制,其底层实现涉及对象头、Monitor、锁升级等多个复杂概念。了解这些原理不仅有助于我们使用 synchronized ,还能在性能调优时做出更合理的选择。

随着Java版本的更新,synchronized 的性能已经得到了大幅提升,在大多数场景下都能提供良好的性能表现。但在高并发场景下,我们仍能需要根据具体情况选择合适的同步策略。

http://www.dtcms.com/a/395904.html

相关文章:

  • 初识C++、其中的引用、类(class)和结构体(struct)
  • Qt之常用控件之QWidget(四)
  • Pod生命周期
  • 【课堂笔记】复变函数-3
  • 深度学习-自然语言处理-序列模型与文本预处理
  • 【C语言】迭代与递归:两种阶乘实现方式的深度分析
  • CLIP多模态模型
  • 快手前端三面(准备一)
  • 前端-JS基础-day1
  • 【开题答辩全过程】以 J2EE在电信行业的应用研究为例,包含答辩的问题和答案
  • C++ QT Json数据的解析
  • RAG——动态护栏
  • Spring Boot 全局鉴权认证简单实现方案
  • 【靶场】webshop渗透攻击
  • 深入浅出现代GPU架构:核心类型、精度模式与选择
  • 开发避坑指南(53):git 命令行标签维护方法
  • javaEE初阶 网络编程(socket初识)
  • 基于Springboot + vue3实现的实验室研究生信息管理系统
  • TwinCat是什么
  • Linux 修炼:进程概念(下)
  • PostgreSQL 全表 count 优化实践:从 SeqScan 痛点分析到 heapam 改进与性能突破
  • 第17讲 机器学习vs神经网络
  • 1. 设计模式--工厂方法模式
  • SpringBoot常用配置
  • 【论文阅读】π0:用于通用机器人控制的视觉-语言-动作流模型
  • Spring 框架学习指南
  • Vue3 父子组件通信实战:props 与 provide/inject 方案对比及用法解析
  • el-image标签预览和VForm打包后项目上层级冲突问题
  • QML学习笔记(九)QML的全局对象
  • element里的select自定义输入的时候,不用点击下拉框选中自定义输入,而是当焦点失去的时候自动赋值输入的内容