
文章目录
-
- 第一课时:synchronized基础与使用
-
- [1.1 从一个线程安全问题开始](#1.1 从一个线程安全问题开始)
- [1.2 synchronized是什么?](#1.2 synchronized是什么?)
- [1.3 初识synchronized的三种用法](#1.3 初识synchronized的三种用法)
-
- [1.3.1 修饰实例方法](#1.3.1 修饰实例方法)
- [1.3.2 修饰静态方法](#1.3.2 修饰静态方法)
- [1.3.3 修饰代码块](#1.3.3 修饰代码块)
- [1.4 深入理解锁的范围](#1.4 深入理解锁的范围)
-
- [1.4.1 三种锁的对比表格](#1.4.1 三种锁的对比表格)
- [1.4.2 常见面试题解析](#1.4.2 常见面试题解析)
- [1.5 synchronized的核心特性](#1.5 synchronized的核心特性)
-
- [1.5.1 可重入性](#1.5.1 可重入性)
- [1.5.2 可见性保证](#1.5.2 可见性保证)
- [1.6 第一课时小结](#1.6 第一课时小结)
- 第二课时:synchronized原理与优化
-
- [2.1 从字节码看synchronized的本质](#2.1 从字节码看synchronized的本质)
-
- [2.1.1 同步代码块的字节码](#2.1.1 同步代码块的字节码)
- [2.1.2 同步方法的字节码](#2.1.2 同步方法的字节码)
- [2.2 Monitor机制深度剖析](#2.2 Monitor机制深度剖析)
-
- [2.2.1 什么是Monitor?](#2.2.1 什么是Monitor?)
- [2.2.2 Monitor的核心数据结构](#2.2.2 Monitor的核心数据结构)
- [2.2.3 锁获取和释放的完整流程](#2.2.3 锁获取和释放的完整流程)
- [2.3 锁升级:从偏向锁到重量级锁](#2.3 锁升级:从偏向锁到重量级锁)
-
- [2.3.1 为什么需要锁升级?](#2.3.1 为什么需要锁升级?)
- [2.3.2 锁的四种状态](#2.3.2 锁的四种状态)
- [2.3.3 偏向锁(Biased Locking)](#2.3.3 偏向锁(Biased Locking))
- [2.3.4 轻量级锁(Lightweight Locking)](#2.3.4 轻量级锁(Lightweight Locking))
- [2.3.5 重量级锁(Heavyweight Locking)](#2.3.5 重量级锁(Heavyweight Locking))
- [2.3.6 锁升级流程总结](#2.3.6 锁升级流程总结)
- [2.4 synchronized与volatile的对比](#2.4 synchronized与volatile的对比)
- [2.5 synchronized与ReentrantLock的对比](#2.5 synchronized与ReentrantLock的对比)
-
- [2.5.1 对比表格](#2.5.1 对比表格)
- [2.5.2 如何选择?](#2.5.2 如何选择?)
- [2.5.3 ReentrantLock使用示例](#2.5.3 ReentrantLock使用示例)
- [2.6 实战:线程安全的单例模式](#2.6 实战:线程安全的单例模式)
-
- [2.6.1 饿汉式(线程安全)](#2.6.1 饿汉式(线程安全))
- [2.6.2 懒汉式(同步方法版)](#2.6.2 懒汉式(同步方法版))
- [2.6.3 双重检查锁(DCL)](#2.6.3 双重检查锁(DCL))
- [2.7 第二课时小结](#2.7 第二课时小结)
- 课程总结

适用对象
本课程适合具备Java基础语法知识,初步了解多线程概念但尚未系统学习并发编程的开发者。无论你是准备面试的求职者,还是希望提升并发编程能力的工程师,这门课程都将为你打下坚实基础。
学习目标
通过两个课时的系统学习,你将能够:
- 掌握 synchronized的三种使用方式及其区别
- 理解 synchronized解决线程安全问题的核心原理
- 剖析 synchronized底层的Monitor机制和锁升级过程
- 区分 synchronized与volatile、ReentrantLock的适用场景
- 应对 面试中关于synchronized的高频问题
课程安排
- 第一课时(约40分钟):基础概念、使用方式、特性详解
- 第二课时(约50分钟):底层原理、锁升级机制、对比与实战
第一课时:synchronized基础与使用
1.1 从一个线程安全问题开始
让我们先看一个经典问题:两个线程同时对共享变量执行自增操作。
java
public class Counter {
private int count = 0;
public void increment() {
count++; // 不是原子操作!
}
public int getCount() {
return count;
}
}
// 测试代码
public class Test {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最终结果: " + counter.getCount()); // 期望20000,实际小于20000
}
}
运行结果:多次执行,结果总是小于20000,有时甚至相差甚远。
问题根源 :count++看似一行代码,但在JVM中分解为三条指令:
- 从主内存读取count到工作内存(读)
- 对count进行加1操作(改)
- 将结果写回主内存(写)
当两个线程同时执行这三步时,可能发生交替执行,导致最后写入的值覆盖了之前的计算结果------这就是典型的竞态条件。
1.2 synchronized是什么?
synchronized是Java提供的一个关键字,译为"同步"。它可以保证在同一时刻,最多只有一个线程执行被它修饰的代码段,从而解决并发环境下的线程安全问题。
从专业角度说,synchronized实现了互斥访问,它保证了三个关键特性:
| 特性 | 说明 |
|---|---|
| 原子性 | 被保护的代码块要么全部执行,要么完全不执行,不会被打断 |
| 可见性 | 一个线程修改共享变量后,其他线程能立即看到最新值 |
| 有序性 | 禁止编译器和处理器对同步块内的代码进行指令重排序 |
1.3 初识synchronized的三种用法
synchronized的使用非常灵活,可以根据需要选择不同的加锁对象。下面我们通过三个典型场景来理解它们的区别。
1.3.1 修饰实例方法
java
public class SyncMethodDemo {
private int count = 0;
// synchronized修饰实例方法,锁是当前对象this
public synchronized void increment() {
count++;
}
}
特点:
- 锁对象是调用该方法的实例对象(即this)
- 同一个对象的多个synchronized实例方法,共用同一把锁
- 不同对象的方法互不干扰
示意图:
线程A ──> obj1.increment() ──> 获取obj1锁 ──> 执行
线程B ──> obj1.increment() ──> 等待obj1锁
线程C ──> obj2.increment() ──> 获取obj2锁 ──> 执行(与A不冲突)
1.3.2 修饰静态方法
java
public class SyncStaticDemo {
private static int count = 0;
// synchronized修饰静态方法,锁是当前类的Class对象
public static synchronized void increment() {
count++;
}
}
特点:
- 锁对象是当前类的Class对象(如SyncStaticDemo.class)
- 所有实例对象共享同一把类锁
- 静态方法锁与实例方法锁互不干扰
思考题:如果一个类既有synchronized静态方法,又有synchronized实例方法,两个线程分别调用它们,会互斥吗?
- 答案:不会。因为一个是类锁,一个是对象锁,两把不同的锁。
1.3.3 修饰代码块
java
public class SyncBlockDemo {
private Object lock = new Object();
private int count = 0;
public void increment() {
// 同步代码块,锁是指定的lock对象
synchronized (lock) {
count++;
}
}
public void decrement() {
// 也可以锁this
synchronized (this) {
count--;
}
}
}
特点:
- 锁对象可以任意指定,灵活性最高
- 可以精确控制同步范围,提高并发性能
- 常用锁对象:this、自定义锁对象、类.class
1.4 深入理解锁的范围
1.4.1 三种锁的对比表格
| 使用形式 | 锁对象 | 作用范围 | 典型应用场景 |
|---|---|---|---|
| 修饰实例方法 | 当前实例对象 | 单个实例内 | 保护实例变量 |
| 修饰静态方法 | 当前类的Class对象 | 所有实例间 | 保护静态变量 |
| 修饰代码块 | 指定的任意对象 | 代码块内 | 细粒度控制 |
1.4.2 常见面试题解析
问题1:synchronized修饰代码块可以给类加锁吗?
当然可以!只要在括号内传入类名.class即可。例如:
java
synchronized (SyncBlockDemo.class) {
// 这是类锁
}
问题2:构造方法可以用synchronized修饰吗?
不可以!Java语法规定构造方法不能是同步的。实际上,构造方法本身就是线程安全的,因为JVM会保证在对象初始化完成前,其他线程无法访问该对象。
问题3:静态同步方法和非静态同步方法同时调用会互斥吗?
不会互斥。假设线程A调用实例对象的非静态同步方法,线程B同时调用该对象所属类的静态同步方法,它们持有的是两把不同的锁(对象锁 vs 类锁),因此可以并行执行。
1.5 synchronized的核心特性
1.5.1 可重入性
概念:同一个线程在持有锁的情况下,可以再次获取同一把锁,而不会被阻塞。
java
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("进入methodA");
methodB(); // 直接调用,不会死锁
}
public synchronized void methodB() {
System.out.println("进入methodB");
}
}
为什么需要可重入?
如果没有可重入性,线程在调用methodA获取锁后,调用methodB时发现自己还要申请同一把锁,就会形成死锁。这显然不合理。
实现原理 :每个锁关联一个持有线程 和一个计数器。线程第一次获取锁时,计数器置为1;同一线程再次获取时,计数器递增;释放一次,计数器递减。直到计数器归零,锁才真正释放。
1.5.2 可见性保证
synchronized不仅能保证原子性,还能保证内存可见性。JMM(Java内存模型)规定:
- 线程释放锁时,会将工作内存中的共享变量刷新到主内存
- 线程获取锁时,会清空工作内存,从主内存重新读取共享变量
这就确保了:一个线程修改的共享变量,在释放锁后,其他线程能立即看到最新值。
1.6 第一课时小结
- synchronized是Java内置的同步关键字,解决原子性、可见性、有序性问题
- 三种使用方式:实例方法(锁this)、静态方法(锁Class对象)、代码块(锁任意对象)
- 核心特性:可重入性、可见性、互斥性
- 不同锁对象决定了不同的同步范围
第二课时:synchronized原理与优化
2.1 从字节码看synchronized的本质
2.1.1 同步代码块的字节码
先看一段简单的同步代码块:
java
public class SyncCodeBlock {
public void doSomething() {
synchronized (this) {
System.out.println("hello");
}
}
}
使用javap -c -v反编译后,关键字节码如下:
3: monitorenter // 进入同步块,获取监视器锁
4: getstatic // 调用System.out
7: ldc // 加载字符串
9: invokevirtual // 调用println
12: aload_1
13: monitorexit // 退出同步块,释放监视器锁
14: goto 22 // 正常结束跳转
17: astore_2 // 异常处理开始
18: aload_1
19: monitorexit // 异常时也释放锁
20: aload_2
21: athrow // 抛出异常
22: return
关键发现:
- 同步代码块使用
monitorenter和monitorexit指令 - 有两个
monitorexit:一个正常退出,一个异常退出,确保锁一定被释放 - 这就是为什么synchronized即使抛出异常也不会死锁
2.1.2 同步方法的字节码
java
public synchronized void syncMethod() {
System.out.println("hello");
}
反编译结果:
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 注意这个标志
Code:
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return
同步方法没有monitorenter/monitorexit指令,而是通过方法表flags中的ACC_SYNCHRONIZED标识。JVM根据这个标识判断是否需要获取锁。
结论:两种实现方式本质上都是获取对象的监视器锁(Monitor Lock),只是表现形式不同。
2.2 Monitor机制深度剖析
2.2.1 什么是Monitor?
Monitor(监视器)是操作系统中用于实现线程同步的机制。Java中的每个对象都与一个Monitor关联,这个关联关系存储在对象头的Mark Word中。
可以把Monitor理解为一个"接待室",里面有三个关键区域:
┌─────────────────────┐
│ Owner │ ← 当前持有锁的线程
├─────────────────────┤
│ EntryList │ ← 等待获取锁的线程队列
├─────────────────────┤
│ WaitSet │ ← 调用了wait()的线程队列
└─────────────────────┘
2.2.2 Monitor的核心数据结构
在HotSpot虚拟机中,Monitor由C++的ObjectMonitor类实现,关键字段如下:
| 字段 | 作用 |
|---|---|
_owner |
指向当前持有锁的线程 |
_EntryList |
等待获取锁的线程队列 |
_WaitSet |
调用了wait()的线程队列 |
_recursions |
记录锁的重入次数 |
_count |
记录线程获取锁的次数 |
2.2.3 锁获取和释放的完整流程
以两个线程T0和T1竞争锁为例:
步骤1:线程T0尝试获取锁
- 根据对象头找到对应的ObjectMonitor
- 检查
_owner是否为null - 通过CAS操作将
_owner设为T0,_count设为1 - T0获取锁成功,进入同步块执行
步骤2:线程T1尝试获取锁
- 此时
_owner为T0,T1 CAS失败 - T1会先自旋几次尝试(适应性自旋)
- 如果T0很快释放锁,T1就成功获取
- 如果T0未释放,T1进入
_EntryList阻塞等待
步骤3:T0释放锁
- 执行
monitorexit,_count减1 - 若
_count变为0,_owner设为null - 唤醒
_EntryList中的线程(通常是队首线程) - T1被唤醒,重新竞争锁
2.3 锁升级:从偏向锁到重量级锁
早期Java的synchronized性能较差,被称为"重量级锁"。JDK 1.6之后引入了一系列优化,让synchronized的性能大幅提升。这就是著名的锁升级机制。
2.3.1 为什么需要锁升级?
想象一下这些场景:
- 场景A:只有一个线程访问同步代码,根本不需要锁竞争
- 场景B:两个线程交替访问,几乎没有同时竞争
- 场景C:多个线程激烈竞争,需要操作系统级别的锁
如果用重量级锁统一处理所有场景,场景A和B会造成不必要的性能开销。锁升级就是让synchronized能够根据竞争激烈程度,动态调整锁的"重量"。
2.3.2 锁的四种状态
从低到高,锁有四种状态:
- 无锁状态
- 偏向锁
- 轻量级锁
- 重量级锁
锁可以升级,但不能降级(除了一次GC清理)。
2.3.3 偏向锁(Biased Locking)
适用场景:只有一个线程反复获取同一把锁。
原理:
- 第一次获取锁时,通过CAS将线程ID记录到对象头
- 之后该线程再来时,只需检查对象头中是否是自己ID
- 如果是,直接进入,无需任何同步操作
示例代码:
java
public class BiasedLockDemo {
private static List<Integer> list = new Vector<>(); // Vector的方法都是同步的
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
list.add(i); // 同一线程反复获取锁
}
}
}
优点 :几乎没有加锁开销,性能极高。
缺点:一旦有其他线程尝试竞争,偏向锁立即撤销并升级。
注意:从JDK 15开始,偏向锁被标记为废弃,未来可能移除。因为在实际应用中,它的性能提升有限,且维护成本高。
2.3.4 轻量级锁(Lightweight Locking)
适用场景 :两个线程交替执行同步块,没有真正的竞争。
原理:
- 线程在自己的栈帧中创建Lock Record,存储对象头的拷贝
- 通过CAS尝试将对象头指向Lock Record
- 如果成功,获取轻量级锁
- 如果失败,说明有竞争,升级为重量级锁
轻量级锁的解锁:
- 通过CAS将对象头恢复为原Mark Word
- 如果恢复成功,解锁完成
- 如果失败,说明已经升级为重量级锁,走重量级锁解锁流程
2.3.5 重量级锁(Heavyweight Locking)
适用场景:多个线程激烈竞争。
原理:
- 线程进入
_EntryList队列阻塞 - 依赖操作系统Mutex Lock实现
- 涉及用户态和内核态切换,开销较大
2.3.6 锁升级流程总结
偏向锁
│
│ 有其他线程尝试获取
↓
轻量级锁
│
│ 有真实竞争
↓
重量级锁
2.4 synchronized与volatile的对比
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | ✅ 保证 | ❌ 不保证 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 保证 | ✅ 保证(禁止重排序) |
| 用法 | 修饰方法或代码块 | 修饰变量 |
| 性能 | 较重(但有优化) | 极轻 |
| 适用场景 | 复合操作 | 单一变量状态标志 |
volatile适用示例:
java
public class VolatileDemo {
private volatile boolean flag = true; // 只作为状态标志
public void stop() {
flag = false; // 简单写操作
}
public void run() {
while (flag) {
// 执行任务
}
}
}
2.5 synchronized与ReentrantLock的对比
2.5.1 对比表格
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM关键字 | Java API(基于AQS) |
| 锁释放 | 自动释放 | 必须手动unlock |
| 公平性 | 非公平 | 可设置公平/非公平 |
| 可中断 | 不支持 | 支持lockInterruptibly() |
| 超时获取 | 不支持 | 支持tryLock(timeout) |
| 尝试获取 | 不支持 | 支持tryLock() |
| 条件变量 | 一个等待集 | 多个Condition |
| 锁状态查询 | 不支持 | 支持查询持有线程等 |
2.5.2 如何选择?
优先使用synchronized的情况:
- 同步逻辑简单,不需要高级功能
- 希望代码简洁不易出错
- JVM持续优化,性能已很好
选择ReentrantLock的情况:
- 需要公平锁
- 需要尝试获取锁或超时获取
- 需要可中断的锁获取
- 需要多个条件变量(如生产者-消费者模式)
- 高竞争场景需要更精细控制
2.5.3 ReentrantLock使用示例
java
ReentrantLock lock = new ReentrantLock(true); // 公平锁
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 标准使用模式
lock.lock();
try {
// 临界区代码
while (条件不满足) {
notEmpty.await(); // 等待
}
// 执行操作
notFull.signal();
} finally {
lock.unlock(); // 必须释放
}
2.6 实战:线程安全的单例模式
综合运用synchronized,实现几种经典的单例模式。
2.6.1 饿汉式(线程安全)
java
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
2.6.2 懒汉式(同步方法版)
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
2.6.3 双重检查锁(DCL)
java
public class DCLSingleton {
// volatile保证可见性和禁止重排序
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
为什么需要volatile?
instance = new DCLSingleton()不是原子操作,可能发生指令重排序。如果不加volatile,其他线程可能拿到一个未初始化完成的对象。这是DCL的关键细节。
2.7 第二课时小结
- synchronized底层基于Monitor实现,字节码层面使用monitorenter/monitorexit或ACC_SYNCHRONIZED
- 锁升级机制(偏向锁→轻量级锁→重量级锁)大幅提升了性能
- synchronized保证原子性、可见性、有序性,是全面的同步工具
- 与volatile相比,synchronized更重量但功能更全
- 与ReentrantLock相比,synchronized简单易用,ReentrantLock功能更丰富
- 实际开发中,根据场景选择合适的同步机制
课程总结
通过两个课时的学习,我们全面掌握了synchronized关键字:
- 第一课时:从线程安全问题出发,学习了synchronized的三种使用方式、锁的范围、可重入性等基础概念
- 第二课时:深入底层,剖析了Monitor机制、锁升级流程,并对比了volatile和ReentrantLock
synchronized作为Java最基础的同步工具,虽然简单,但背后的原理并不简单。理解它的实现机制,不仅能帮助我们写出更正确的并发程序,还能在面试中脱颖而出。
课后思考题
- 如果一个线程在同步块中抛出异常,锁会自动释放吗?为什么?
- 偏向锁在JDK 15中被废弃的原因是什么?谈谈你的理解。
- 如何用synchronized实现一个阻塞队列?
- 为什么说synchronized是"悲观锁",而CAS是"乐观锁"?