目录
概述
synchronized 是用于实现线程同步的核心关键,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能阻塞在那里。所以,它能确保同一时刻只有一个线程可以执行被保护的代码块或方法,从而避免多线程环境下的数据竞争和不一致问题。
锁升级
Java 6 之后对 synchronized 进行了改进,引入了锁升级机制,锁状态会随着不同的线程竞争情况,逐渐升级。
所以,锁升级,指锁状态从低级别向高级别逐步转变的过程,方向为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。锁升级,减少传统重量级锁的性能开销,在不同竞争程度下选择最优的锁实现,选择最优的锁实现。
- 偏向锁:单线程环境下,锁会偏向第一个获取它的线程,后续该线程再次获取锁时无需竞争。
- 轻量级锁:多线程交替执行同步块时,通过 CAS(Compare and Swap)操作尝试获取锁,避免线程阻塞。
- 重量级锁:多线程同时竞争锁时,锁会升级为重量级锁,此时线程会被阻塞,性能开销较大。
偏向锁
偏向锁的核心思想是,假设需要加锁的同步代码,只有一个线程在调用,如果发现有多个线程调用,升级成轻量级锁。
偏向锁:单线程环境下,锁会偏向第一个获取它的线程,后续该线程再次获取锁时无需竞争。
适用场景:单线程环境,锁总是被同一个线程获取。
原理:
- 当线程首次获取锁时,JVM 在对象头的 Mark Word 中记录该线程 ID(偏向锁状态)。
- 后续该线程再次获取锁时,无需任何同步操作,直接对比 Mark Word 中的线程 ID。
升级条件:当其他线程尝试竞争该锁时,偏向锁撤销并升级为轻量级锁。
轻量级锁
“轻量级锁”的概念,是相对于“使用操作系统互斥锁来实现的重量级锁”,但轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一把锁的情况,就会导致轻量级锁升级为重量级锁。
轻量级锁:多线程交替执行同步块时,通过 CAS(Compare and Swap)操作尝试获取锁,避免线程阻塞。
适用场景:多线程交替执行同步块,无实际竞争。
原理:
- 线程进入同步块时,JVM 在当前线程的栈帧中,创建锁记录(Lock Record)。
- 通过 CAS(Compare and Swap)操作将对象头的 Mark Word 复制到锁记录,并将 Mark Word 指向锁记录地址。
- 若 CAS 成功,线程获取轻量级锁;若失败,说明存在竞争,锁升级为重量级锁。
升级条件:多个线程同时竞争同一把锁时。
重量级锁
“重量级锁”依赖于操作系统互斥锁(Mutex Lock)所实现的锁。操作系统的互斥锁实现线程之间的切换,需要从用户态转换到内核态,切换成本非常高,状态之间的转换需要相对比较长的时间,这是早期Synchronized效率低的原因。因此,这种依赖于操作系统互斥锁(Mutex Lock)所实现的锁,称之为“重量级锁”。
用户态和内核态,代表两种不同的CPU状态。内核态(Kernel Mode)用于运行操作系统程序,用户态(User Mode)用于运行用户程序。
重量级锁:多线程同时竞争锁时,锁会升级为重量级锁,此时线程会被阻塞,性能开销较大。
适用场景:多线程同时竞争锁,频繁发生上下文切换。
原理:
- 依赖操作系统的互斥量(Mutex)实现,线程竞争失败会被挂起(park),进入等待队列。
- 锁释放时,唤醒等待队列中的线程重新竞争。
性能开销:涉及用户态和内核态的切换,成本较高。
锁升级过程
- 无锁 → 偏向锁:当一个线程,首次访问同步代码块并获取锁时,会在对象头的 Mark Word 中存入该线程ID。此时,锁的状态为偏向锁。偏向锁不会主动释放,只有当其他线程尝试竞争时才会撤销。
- 偏向锁 → 轻量级锁:当有另一个线程,尝试访问同一同步块时,偏向锁会升级为轻量级锁。轻量级锁通过 CAS(Compare and Swap)操作尝试获取锁(将Mark Word 复制到Lock Record中),避免线程阻塞。
- 轻量级锁 → 重量级锁:如果多个线程交替访问同步块,轻量级锁可以避免线程阻塞;但如果同一时间多个线程竞争锁,轻量级锁会升级为重量级锁,此时线程会被阻塞,性能开销较大。
优化偏向锁
- 对于明确多线程竞争的场景,可通过 -XX:-UseBiasedLocking 禁用偏向锁,直接使用轻量级锁;
- 调整 -XX:BiasedLockingStartupDelay=0 取消偏向锁启动延迟,默认值:4000 毫秒(JDK 8),即 JVM 启动后 4 秒才启用偏向锁。设计初衷:JVM 启动初期,系统负载较高,存在较多线程竞争,此时启用偏向锁可能会频繁触发锁撤销,反而影响性能。减少应用启动初期的锁升级开销。
锁降级
锁降级,指锁状态从高级别向低级别转变的过程。但是,锁降级的场景非常有限,且需满足特定条件:在 GC垃圾回收的过程中,若锁对象不在被引用,GC进行垃圾回收,并将锁状态,被标记为不再使用,JVM 会将锁状态重置为无锁。
synchronized实现原理
synchronized 实现原理主要基于对象头和 Monitor(监视器)机制。
对象头:每个 Java 对象都有一个对象头,其中的 Mark Word 会记录锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
Monitor:每个对象都关联一个 Monitor监视器,由它来实现线程的同步机制。所以,线程获取锁,实际上是获取锁对象关联的 Monitor监视器,未获取到的线程会被阻塞,并进入等待队列。
对象头
在Hotspot虚拟机中,一个JAVA对象的存储结构,在内存中的存储布局分为 3 块区域:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。每个 Java 对象都有一个对象头,锁的状态,保存在对象头中。
对象头(Object Header)又包含两部分:Mark Word和Klass Pointer。
Mark Word
长度:32 位 JVM 中占 32 位(4 字节),64 位 JVM 中占 64 位(8 字节)。
作用:存储对象的哈希值、GC分代年龄、锁状态等运行时数据。其中,Mark Word 记录的锁状态主要包括:无锁、偏向锁、轻量级锁、重量级锁。
特点:Mark Word 的位布局会根据对象的锁状态动态变化。
所以,当对象被 synchronized 修饰时,锁信息会被记录在 Mark Word 中。根据锁的状态不同,Mark Word 会存储不同的内容。
Klass Pointer
长度:32 位 JVM 中占 32 位(4 字节),64 位 JVM 中默认开启指针压缩占 32 位(4 字节),不开启时占 64 位(8 字节)。
作用:指向对象所属类的元数据(Class 对象),JVM 通过该指针确定对象是哪个类的实例。
指针压缩(Compressed OOPs)
作用:64 位 JVM 中,通过压缩指针将 Klass Pointer 从 8 字节压缩为 4 字节,减少内存占用。
开启方式:默认开启(-XX:+UseCompressedOops),堆内存超过 32GB 时自动关闭指针压缩,恢复为8字节
Monitor(监视器)机制
synchronized代码块在字节码层面,是由monitorenter/monitorexit指令定义完成。这两个指令,实现了监视器(monitor)机制。
Monitor 是 Java 中实现同步的基础,本质是一个同步工具,也可以理解为一种锁的实现。每个 Java 对象都可以关联一个 Monitor监视器;
当一个线程尝试访问被 synchronized 保护的代码块时,也其实就相当于,线程通过执行monitorenter指令尝试获取monitor的所有权:
- 获取 Monitor:线程会首先检查对象的Mark Word 是否指向当前线程的锁记录(轻量级锁),或是否指向 Monitor 对象(重量级锁)。
- 竞争锁:如果 Monitor 已被其他线程占用,则当前线程会被阻塞,进入Entry List 队列等待。
- 释放锁:持有锁的线程执行完同步代码块后,会释放 Monitor,并唤醒 Entry List 中的等待线程重新竞争。
synchronized的性能问题
- 锁粒度太大:同步范围覆盖过多无关代码,导致线程竞争加剧。
- 锁持有时间过长:同步块中包含耗时操作(如 IO、网络请求)。
- 锁竞争激烈:多个线程频繁争抢同一把锁,导致上下文切换频繁。
- 锁升级频繁:大量竞争导致锁从偏向锁升级到重量级锁。
- 死锁:线程互相等待对方释放锁,导致系统停滞。
线程安全的单例模式
// 静态内部类方式(懒加载、线程安全)
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 类加载时初始化,线程安全
}
}
// 枚举方式(最简洁、天然防止反射和反序列化破坏)
public enum Singleton {
INSTANCE;
public void doSomething() {
// ...
}
}