Java中synchronized的实现原理与应用

本文详细介绍了Java中Synchronized的实现原理,包括其基于对象头MarkWord存储锁状态的机制,MonitorRecord的数据结构,以及锁的升级过程。从无锁状态到重量级锁状态的变化,解释了每种锁状态的特点及其应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java中的每一个对象都可以作为锁,而在Synchronized实现同步的几种方式中分别为:

  • 普通同步方法:锁是当前实例对象
  • 静态同步方法:锁是当前类的Class对象
  • 同步方法块:锁是Synchronized括号里配置的对象

任何一个对象都一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。

Java对象头

synchronized使用的锁是存放在Java对象头里面,具体位置是对象头里面的MarkWord,MarkWord里默认数据是存储对象的HashCode等信息,但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式,可能值如下所示:

这里写图片描述
无锁状态 : 对象的HashCode + 对象分代年龄 + 状态位001

Monitor Record

Monitor Record是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor record关联(对象头的MarkWord中的LockWord指向monitor record的起始地址),同时monitor record中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。如下图所示为Monitor Record的内部结构

Monitor Record
Owner
EntryQ
RcThis
Nest
HashCode
Candidate

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;

EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。

RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。

Nest:用来实现重入锁的计数。

HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

锁的类型

Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

                                 无锁 --> 偏向锁 --> 轻量级 --> 重量级

偏向锁

引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。

加锁:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程(此时会引发竞争,偏向锁会升级为轻量级锁)。

膨胀过程:当前线程执行CAS获取偏向锁失败(这一步是偏向锁的关键),表示在该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁所有权。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,并从偏向锁所有者的私有Monitor Record列表中获取一个空闲的记录,并将Object设置LightWeight Lock状态并且Mark Word中的LockRecord指向刚才持有偏向锁线程的Monitor record,最后被阻塞在安全点的线程被释放,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行同步代码。

偏向锁

轻量级锁

引入背景:这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒

加锁
(1)当对象处于无锁状态时(RecordWord值为HashCode,状态位为001),线程首先从自己的可用moniter record列表中取得一个空闲的moniter record,初始Nest和Owner值分别被预先设置为1和该线程自己的标识,一旦monitor record准备好然后我们通过CAS原子指令安装该monitor record的起始地址到对象头的LockWord字段,如果存在其他线程竞争锁的情况而调用CAS失败,则只需要简单的回到monitorenter重新开始获取锁的过程即可。

(2)对象已经被膨胀同时Owner中保存的线程标识为获取锁的线程自己,这就是重入(reentrant)锁的情况,只需要简单的将Nest加1即可。不需要任何原子操作,效率非常高。

(3)对象已膨胀但Owner的值为NULL,当一个锁上存在阻塞或等待的线程同时锁的前一个拥有者刚释放锁时会出现这种状态,此时多个线程通过CAS原子指令在多线程竞争状态下试图将Owner设置为自己的标识来获得锁,竞争失败的线程在则会进入到第四种情况(4)的执行路径。

(4)对象处于膨胀状态同时Owner不为NULL(被锁住),在调用操作系统的重量级的互斥锁之前先自旋一定的次数,当达到一定的次数时如果仍然没有成功获得锁,则开始准备进入阻塞状态,首先将rfThis的值原子性的加1,由于在加1的过程中可能会被其他线程破坏Object和monitor record之间的关联,所以在原子性加1后需要再进行一次比较以确保LockWord的值没有被改变,当发现被改变后则要重新monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。

轻量级锁

不同锁的比较

这里写图片描述

参考

方腾飞:Java SE1.6中的Synchronized
JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)

<think>我们正在讨论Java中的synchronized关键字。根据引用内容,synchronized关键字用于实现线程同步,防止多个线程同时执行同步代码块,从而避免竞态条件和数据不一致问题。它的底层实现涉及到对象头中的Mark Word、升级过程(偏向轻量级、重量级)等。 用户问题:理解Javasynchronized关键字的实现原理及其在多线程编程中的应用场景 回答结构: 1. synchronized关键字的作用 2. synchronized的底层实现原理(包括对象头、升级过程) 3. synchronized在多线程中的应用场景(示例) 4. 注意事项 根据引用[1][2][3][4][5],我们可以整理如下: 1. 作用:确保多个线程在访问共享资源时的协调和同步,避免竞态条件。 2. 实现原理: - 每个Java对象都有一个对象头,其中包含Mark Word(存储信息)。 - Mark Word在不同状态下存储的内容不同(无偏向轻量级、重量级)。 - 升级过程(优化): 无 -> 偏向(当只有一个线程访问时) -> 轻量级(当有多个线程交替访问,但未同时竞争) -> 重量级(当有多个线程同时竞争) 3. 应用场景:多线程环境下对共享资源的访问控制,例如: - 单例模式的双重检查定 - 线程安全的计数器 - 共享数据结构的操作(如Vector、Hashtable等) 4. 注意事项:synchronized是重量级(在升级到重量级时)会涉及到操作系统状态的切换,影响性能。但在JDK1.6后进行了优化,引入了偏向轻量级,减少了性能开销。 下面详细展开:</think>### Java synchronized 关键字实现原理应用场景 #### 一、synchronized 的作用 `synchronized` 是 Java 中的**同步关键字**,用于解决多线程环境下的**线程安全问题**: 1. **防止竞态条件**:确保同一时刻只有一个线程访问共享资源。 2. **保证可见性**:线程对共享变量的修改对其他线程立即可见。 3. **实现有序性**:禁止指令重排序优化。 > 典型应用场景:多线程卖票系统、单例模式、共享计数器、线程安全的集合类(如 `Vector`)[^2]。 --- #### 二、底层实现原理 ##### 1. 对象头 Mark Word 每个 Java 对象在内存中包含**对象头**(Object Header),其中关键部分是 **Mark Word**(占 64 位 JVM 中的 64 位): | 状态 | Mark Word 存储内容 | 标志位 | |--------------|-------------------------------------|--------| | **无** | 哈希码 + 分代年龄 | `01` | | **偏向** | 线程 ID + 时间戳 + 分代年龄 | `101` | | **轻量级** | 指向栈中记录的指针 | `00` | | **重量级** | 指向互斥量(monitor)的指针 | `10` | ##### 2. 升级流程 JDK 1.6 后引入**升级优化**(避免直接使用重量级): ```mermaid graph LR A[无] -->|首次获取| B[偏向] B -->|发生竞争| C[轻量级] C -->|自旋失败| D[重量级] ``` - **偏向**:仅一个线程访问时,通过 CAS 将线程 ID 写入 Mark Word。 - **轻量级**:多个线程交替执行,通过 CAS 和自旋尝试获取。 - **重量级**:竞争激烈时,线程进入阻塞状态,由操作系统管理互斥量(monitor)[^3][^4]。 ##### 3. Monitor 机制 - 当升级到重量级时,Mark Word 指向 **ObjectMonitor** 对象(C++ 实现)。 - ObjectMonitor 包含: - `_owner`:持有的线程 - `_EntryList`:阻塞等待的线程队列 - `_WaitSet`:调用 `wait()` 的线程队列 > 同步代码块编译后生成 `monitorenter` 和 `monitorexit` 指令,由 JVM 调用 ObjectMonitor 的机制[^5]。 --- #### 三、应用场景示例 ##### 1. 同步代码块(显式对象) ```java public class Counter { private int count = 0; private final Object lock = new Object(); // 专用对象 public void increment() { synchronized(lock) { // 定特定对象 count++; } } } ``` ##### 2. 同步方法(当前实例) ```java public synchronized void safeWrite() { // 定当前实例(this) } ``` ##### 3. 静态同步方法( Class 对象) ```java public static synchronized void staticSafeMethod() { // 定类的 Class 对象(如 Counter.class) } ``` --- #### 四、性能优化建议 1. **减小同步范围**:尽量使用同步代码块而非同步方法。 2. **避免嵌套**:防止死和性能下降。 3. **考虑替代方案**:高并发场景用 `ReentrantLock` 或 `StampedLock`。 4. **分离技术**:如 `ReadWriteLock` 分离读/写。 > 统计表明:80% 的在无竞争时通过偏向/轻量级可降低开销至纳秒级[^3]。 --- #### 五、常见问题解析 1. **Q: 为什么 `String` 不适合作为对象?** A: 字符串常量池可能被多个类共享,导致意外竞争。 2. **Q: `synchronized` 和 `volatile` 的区别?** A: `volatile` 仅保证可见性和有序性,不保证原子性;`synchronized` 三者均保证。 3. **Q: 升级是否可逆?** A: 偏向可撤销回无状态;重量级无法降级。 ---
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值