编辑
Hello大家好!👋 我是摘星✨,今天我们来深度拆解Java并发编程中最经典的「悲观锁」🔒设计。
在多线程环境下,当你的转账操作被重复提交💸、库存被超卖📉、计数器结果离奇错误❌时,背后往往是因为缺乏合理的锁控制。而悲观锁作为Java并发中最「简单粗暴」的解决方案,从JDK1.0时代的重量级锁⛓️,到如今JVM层级的锁升级优化⚡,其底层实现堪称一部高性能并发的发展史📜。
本文将带你穿透**synchronized
关键字**的表面语法,直击三大核心问题💡:
- **🤔 为什么悲观锁能保证线程安全?**(从Java对象头到Monitor的硬件级协作)
- **⚡ JDK1.6后
synchronized
如何实现性能飞跃?**(偏向锁/轻量级锁的取舍智慧) - **🚫 高并发场景下如何规避锁的性能陷阱?**(从字节码层面理解锁膨胀的条件)
📌 举个真实案例:某电商平台在秒杀活动中使用synchronized
导致TPS从8000📈暴跌到300📉,最终通过缩小锁粒度+锁分离优化提升15倍性能🚀——我们将在文中用代码还原这个优化过程。
下面我们直接切入正题,从操作系统与JVM的协作契约🤝开始讲起!
目录
4. 有锁并发-悲观锁
4.1. 悲观锁思想
悲观锁:假设其他线程会修改共享资源,在进入同步代码块时就加上锁,防止其他线程的干扰。但是如果竞争激烈,就会发生线程上下文切换,性能相对低。
4.2. Synchronized
synchronized
关键字,基于Monitor实现,用于实现多线程之间的同步,是悲观锁
synchronized
的执行流程:
- 第一个线程获取对象锁执行同步代码块
- 其他想要获取对象锁进入同步代码块的线程进入阻塞状态
- 第一个线程退出同步代码块释放锁后,会唤醒阻塞中的线程
- 被唤醒的线程进行非公平的锁争抢,抢到锁的线程执行同步代码块,其他线程继续阻塞
synchronized
的特性:
- 原子性(Atomicity):
synchronized
保证同时只能有一个线程执行同步代码块,对共享数据的操作是原子的。 - 可见性(Visibility):一个线程执行
synchronized
同步代码块时,会先获取主内存中的数据到工作内存中,修改共享资源,退出同步代码块时,会将工作内存中的数据刷新到主内存中,保证数据的可见性。 - 有序性(Ordering):
synchronized
会禁止JVM对字节码指令重排优化,保证线程执行的有序性。
synchronized
的作用范围:
- 成员方法:
synchronized
修饰成员方法时,锁住的是当前对象实例。 - 静态方法:
synchronized
修饰静态方法时,锁定的是当前类的Class对象。 - 代码块:
synchronized
修饰代码块时,锁定的是指定的对象。
多线程只有获取同一个对象的锁才能起到同步互斥的效果
JDK1.6之前synchronized是重量级锁,jdk1.6之后对synchronized做了一系列优化,加入了偏向锁和轻量级锁,优化了线程上下文切换的性能
4.3. Java对象头
synchronized是悲观锁,在操作共享资源之前需要先加锁,加的锁就存在于Java对象头中.
Hotspot虚拟机中的对象头主要包含两部分数据:Mark Word(标记字段)和Klass Pointer(类型指针)
- Mark Word:存储对象的hashcode,分代年龄,锁标志位信息.这些信息跟对象自定定义无关,所以Mark Word被设计为非固定的数据结构以便在极小的空间中存储多的数据,它会根据对象的状态复用空间,也就是说在运行期间,Mark Word中的存储数据会跟着锁标志位变化而变化
编辑 - Klass Pointer:指向类元数据的指针,用于确定当前对象是哪个类的实例
4.4. Monitor
Monitor是一种同步机制,依赖于操作系统底层的Mutex Lock(同步锁)来实现同步,在Java中,每一个Java对象都会关联一个Monitor对象,synchronized关键字就是通过Monitor来实现线程同步的,是重量级锁
Monitor的组成部分:
- owner(所有者):用于存放当前拥有了Monitor对象的线程,也就是拥有对象锁的线程,只有owner线程才能执行同步代码块.
- waitlist(等待队列):用于存放因调用了wait方法而处于等待中的线程,同时释放该对象锁
- EntryList(入口队列):用于存放尝试获取对象锁未成功而处于阻塞状态中的线程,当owner释放对象锁时,会唤醒EntryList中的线程来竞争对象锁,成功者成为会owner,失败者则继续等待
Monitor的执行流程:
- 第一个进入
synchronized
代码块的线程,首先尝试获取对象锁,如果锁是空闲状态,则获取到锁,成为owner。如果锁已被其他线程占用,则进入阻塞 - 其他访问
synchronized
代码块的线程尝试获取对象锁未成功,处于阻塞状态,进入EntryList队列,等待对象锁的释放 synchronized
代码块执行完毕后,会释放该对象锁,同时唤醒EntryList中阻塞的线程来竞争对象锁,竞争成功的线程成为新的owner,失败的线程则继续等待