第一部分:synchronized基础

理解synchronized关键字在多线程编程中的核心作用

学习目标

掌握synchronized的作用、解决的问题以及基本使用场景

同步问题

多线程环境下,当多个线程同时访问共享资源时,如果没有同步机制,会导致:

  • 数据不一致
  • 竞态条件
  • 不可预知的结果
// 无同步的计数器 public class UnsafeCounter { private int count = 0; public void increment() { count++; // 非原子操作 } public int getCount() { return count; } }

synchronized作用

synchronized关键字提供了一种内置的锁机制,用于:

  • 保证操作的原子性
  • 保证内存可见性
  • 防止指令重排序
// 使用synchronized的计数器 public class SafeCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
线程1: 读取count=0
线程2: 读取count=0
线程1: 增加count=1
线程2: 增加count=1

无同步时,两个线程同时读取和修改共享变量,导致最终结果错误

线程1: 获取锁 → 读取count=0 → 增加count=1 → 释放锁
线程2: 等待锁 → 获取锁 → 读取count=1 → 增加count=2 → 释放锁

使用synchronized后,线程顺序执行共享资源操作,保证结果正确

synchronized工作原理示意图

synchronized保证同一时间只有一个线程可以执行同步代码块

第二部分:synchronized使用方式

掌握synchronized的三种使用方式及适用场景

学习目标

理解实例方法同步、静态方法同步和同步代码块的区别与应用

1. 实例方法同步

锁对象是当前实例对象(this)

public class Example { // 同步实例方法 public synchronized void syncMethod() { // 临界区代码 } }

适用场景: 保护实例变量,防止多个线程同时访问同一个实例的方法

2. 静态方法同步

锁对象是当前类的Class对象

public class Example { // 同步静态方法 public static synchronized void staticSyncMethod() { // 临界区代码 } }

适用场景: 保护静态变量,防止多个线程同时访问类的静态方法

3. 同步代码块

可以指定任意对象作为锁,提供更细粒度的控制

public class Example { private final Object lock = new Object(); public void method() { // 非同步代码 // 同步代码块 synchronized(lock) { // 临界区代码 } // 非同步代码 } }

适用场景: 需要更细粒度的锁控制,减少锁的竞争范围

同步方式 锁对象 作用范围 性能影响
实例方法同步 当前实例对象(this) 单个实例 较高(锁整个方法)
静态方法同步 类的Class对象 所有实例 高(全局锁)
同步代码块 任意指定对象 代码块范围 较低(细粒度控制)

使用建议

1. 优先使用同步代码块,减少锁的范围
2. 避免使用对象锁保护静态变量
3. 使用private final对象作为锁对象
4. 避免在锁代码块中调用外部方法(防止死锁)

第三部分:synchronized工作原理

深入理解synchronized的底层实现机制

学习目标

掌握监视器锁(monitor)机制、对象头结构和内存语义

监视器锁(Monitor)

synchronized基于监视器锁实现,每个Java对象都与一个监视器关联

  • 进入同步代码块时自动获取锁
  • 退出同步代码块时自动释放锁
  • 基于对象头的Mark Word实现

对象头结构

Java对象在内存中的布局:

  • Mark Word(标记字段)
  • Klass Pointer(类型指针)
  • Instance Data(实例数据)
  • Padding(对齐填充)

synchronized使用Mark Word存储锁信息

锁定状态
未锁定状态

synchronized内存语义

synchronized保证内存可见性和有序性:

  • 进入同步块前:从主内存刷新变量值
  • 退出同步块时:将修改刷新到主内存
  • 防止编译器和处理器重排序
public class MemoryVisibility { private boolean flag = false; private int value = 0; public synchronized void writer() { flag = true; // 1 value = 42; // 2 } public synchronized void reader() { if (flag) { // 3 System.out.println(value); // 4 } } }

synchronized保证1和2不会被重排序,3和4不会被重排序,且线程B能看到线程A的所有修改

第四部分:synchronized锁升级

理解Java 6引入的锁优化机制

学习目标

掌握偏向锁、轻量级锁、重量级锁的升级过程

偏向锁

适用于只有一个线程访问同步块的场景

  • 锁对象记录线程ID
  • 同一线程进入无需CAS操作
  • 减少同步开销

轻量级锁

适用于线程交替执行的场景

  • 使用CAS操作获取锁
  • 避免线程阻塞
  • 自旋优化

重量级锁

适用于多线程竞争的场景

  • 使用操作系统的互斥量
  • 线程阻塞进入等待队列
  • 开销最大
锁状态: 无锁
第一次访问 → 偏向锁
第二个线程访问 → 轻量级锁(自旋)
自旋超过阈值 → 重量级锁(阻塞)

锁升级过程

1. 初始状态:无锁
2. 第一个线程访问:升级为偏向锁
3. 第二个线程访问:升级为轻量级锁(自旋)
4. 自旋超过阈值(默认10次):升级为重量级锁
5. 重量级锁使用操作系统互斥量,线程进入阻塞状态

注意事项

1. 锁升级不可逆
2. 偏向锁在竞争激烈时反而降低性能
3. 可通过JVM参数调整锁行为(如-XX:-UseBiasedLocking)
4. 自旋锁消耗CPU,需合理设置自旋次数

第五部分:常见错误与陷阱

避免synchronized使用中的常见问题

1. 锁对象错误

错误地使用可变对象或基本类型作为锁

// 错误示例:使用String作为锁对象 private String lock = "lock"; public void method() { synchronized(lock) { // ... lock = "new lock"; // 修改锁对象 } }

锁对象必须是final不可变对象,避免使用String常量池对象

2. 锁范围过大

同步整个方法或大段代码,降低并发性能

// 错误示例:同步整个方法 public synchronized void process() { // 非临界区代码(耗时操作) loadDataFromDB(); // 临界区代码(需要同步) updateCounter(); // 非临界区代码 saveToFile(); }

解决方案:使用同步代码块,只保护真正需要同步的部分

3. 死锁问题

多个线程互相持有对方需要的锁

// 线程1 synchronized(lockA) { synchronized(lockB) { // ... } } // 线程2 synchronized(lockB) { synchronized(lockA) { // ... } }

解决方案:按固定顺序获取锁,使用tryLock()尝试获取锁

4. 锁粒度不当

过粗或过细的锁粒度影响性能

  • 锁粒度过粗:降低并发度
  • 锁粒度过细:增加锁开销

解决方案:根据业务场景选择合适粒度的锁

5. 忘记释放锁

虽然synchronized会自动释放锁,但异常时需注意

public void method() { synchronized(lock) { // 可能抛出异常的操作 processData(); } }

解决方案:在finally块中确保释放资源(Lock接口),synchronized会自动释放

6. 锁对象不一致

使用不同对象保护同一资源

public class Counter { private int count = 0; private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void increment() { synchronized(lock1) { count++; } } public int getCount() { synchronized(lock2) { // 错误:使用不同锁对象 return count; } } }

必须使用同一个锁对象保护同一资源

第六部分:synchronized最佳实践

高效、安全地使用synchronized

synchronized优点

  • 简单易用,语法简洁
  • 自动释放锁,避免忘记释放
  • JVM内置优化(锁升级)
  • 保证内存可见性和有序性

synchronized缺点

  • 功能有限(不可中断、不可超时)
  • 性能在竞争激烈时下降
  • 无法实现公平锁
  • 锁粒度控制不如Lock灵活

最佳实践1:选择合适的锁对象

  • 使用private final对象作为锁
  • 避免使用字符串常量或基本类型
  • 静态同步使用Class对象
public class BestPractice { // 推荐:使用private final对象 private final Object lock = new Object(); public void method() { synchronized(lock) { // ... } } }

最佳实践2:减小锁范围

  • 优先使用同步代码块而非同步方法
  • 只保护真正需要同步的代码
  • 避免在同步块中执行耗时操作
public void optimizedMethod() { // 非同步操作 loadData(); // 只同步关键部分 synchronized(lock) { updateSharedResource(); } // 非同步操作 saveResult(); }

最佳实践3:避免嵌套锁

  • 尽量避免在同步块内获取其他锁
  • 必须嵌套时,按固定顺序获取锁
  • 使用tryLock()尝试获取锁
// 固定顺序获取锁 public void safeNestedLock() { synchronized(lockA) { synchronized(lockB) { // ... } } } // 避免 public void unsafeNestedLock() { synchronized(lockB) { synchronized(lockA) { // 与上面顺序不一致 // ... } } }

性能优化建议

1. 减少锁持有时间
2. 降低锁粒度(如分段锁)
3. 读写分离(ReadWriteLock)
4. 无锁编程(原子类)
5. 避免热点锁(锁竞争激烈)

第七部分:练习与测验

通过练习巩固synchronized知识

动手练习

  • 实现一个线程安全的计数器
  • 使用synchronized解决生产者-消费者问题
  • 实现一个线程安全的LRU缓存
  • 使用同步代码块优化一个同步方法
1

synchronized知识测验

以下关于synchronized的描述,哪项是错误的?
以下哪种锁对象的使用方式是最佳实践?

深入学习资源

1. 《Java并发编程实战》第2章、第13章
2. Oracle官方文档:Java同步机制
3. JVM规范:synchronized实现原理
4. OpenJDK源码:ObjectMonitor实现