在现代多核处理器时代,编写线程安全的并发程序变得尤为重要。Java 提供了丰富的同步工具类来帮助开发者处理多线程并发问题。本文将系统性地介绍 Java 中的各种同步机制和工具类,帮助您理解它们的原理、使用场景和最佳实践。
一、基础同步机制
1. synchronized 关键字
synchronized
是 Java 中最基本的同步机制,它基于内置锁(monitor)实现。
特性:
- 可重入性:同一个线程可以多次获取同一个锁
- 互斥性:同一时间只有一个线程可以持有锁
- 自动释放:当同步块执行完毕或抛出异常时自动释放锁
使用方式:
// 同步方法
public synchronized void syncMethod() {
// 方法体
}
// 同步代码块
public void syncBlock() {
synchronized(this) {
// 临界区代码
}
}
// 同步静态方法
public static synchronized void staticSyncMethod() {
// 方法体
}
适用场景:
- 简单的线程同步需求
- 需要保护的对象或方法访问不频繁
- 同步代码块执行时间较短
2. volatile 关键字
volatile
提供了一种轻量级的同步机制,主要用于保证变量的可见性和禁止指令重排序。
特性:
- 可见性:保证变量的修改对所有线程立即可见
- 禁止指令重排序:防止JVM优化导致的执行顺序改变
- 不保证原子性:复合操作仍需同步
使用方式:
private volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while(!shutdownRequested) {
// 执行任务
}
}
适用场景:
- 状态标志位
- 单次发布的安全发布模式
- 双重检查锁定模式(Double-Checked Locking)
二、Lock 接口及其实现
java.util.concurrent.locks
包提供了更灵活的锁机制。
1. ReentrantLock
ReentrantLock
是可重入的互斥锁,提供了比 synchronized
更丰富的功能。
特性:
- 可重入性
- 可中断的锁获取
- 尝试非阻塞获取锁
- 超时获取锁
- 公平锁与非公平锁选择
使用方式:
Lock lock = new ReentrantLock();
public void performTask() {
lock.lock(); // 阻塞获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在finally块中释放锁
}
}
// 尝试获取锁
public void tryPerformTask() {
if (lock.tryLock()) {
try {
// 临界区代码
} finally {
lock.unlock();
}
} else {
// 执行替代逻辑
}
}
与 synchronized 对比:
特性 | synchronized | ReentrantLock |
---|---|---|
可重入性 | 支持 | 支持 |
公平锁 | 不支持 | 支持 |
尝试非阻塞获取锁 | 不支持 | 支持 |
可中断的锁获取 | 不支持 | 支持 |
超时获取锁 | 不支持 | 支持 |
条件变量 | 有限支持 | 完全支持 |
2. ReadWriteLock 和 ReentrantReadWriteLock
读写锁分离了读和写的操作,提高了并发性能。
特性:
- 读锁共享:多个线程可以同时持有读锁
- 写锁独占:写锁互斥,与其他读锁和写锁互斥
- 锁降级:写锁可以降级为读锁
使用方式:
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void readData() {
rwLock.readLock().lock();
try {
// 读取数据
} finally {
rwLock.readLock().unlock();
}
}
public void writeData() {
rwLock.writeLock().lock();
try {
// 写入数据
} finally {
rwLock.writeLock().unlock();
}
}
适用场景:
- 读多写少的场景
- 缓存实现
- 需要保证数据一致性的并发访问
三、原子变量类 (CAS 实现)
java.util.concurrent.atomic
包提供了一系列基于 CAS (Compare-And-Swap) 操作的原子类。
1. 基本原子类
AtomicInteger
AtomicLong
AtomicBoolean
使用方式:
AtomicInteger counter = new AtomicInteger(0);
// 原子递增
int newValue = counter.incrementAndGet();
// 原子更新
boolean updated = counter.compareAndSet(expect, update);
2. 引用类型原子类
AtomicReference
AtomicStampedReference
(解决ABA问题)AtomicMarkableReference
AtomicReference<String> atomicRef = new AtomicReference<>("initial");
// 原子更新引用
atomicRef.compareAndSet("initial", "updated");
// 带版本戳的引用
AtomicStampedReference<String> stampedRef =
new AtomicStampedReference<>("value", 0);
3. 数组原子类
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
AtomicIntegerArray array = new AtomicIntegerArray(10);
array.incrementAndGet(0); // 原子更新数组元素
CAS 原理:
CAS 是一种无锁算法,包含三个操作数:
- 内存位置(V)
- 期望值(A)
- 新值(B)
当且仅当 V 的值等于 A 时,CAS 才会将 V 的值更新为 B,否则不做任何操作。
适用场景:
- 计数器
- 状态标志
- 非阻塞算法实现
- 需要高性能的并发控制
四、高级同步工具类
1. CountDownLatch
允许一个或多个线程等待其他线程完成操作。
特性:
- 一次性使用,计数不可重置
- 基于AQS实现
- 不可重复使用
使用方式:
// 初始化计数器
CountDownLatch latch = new CountDownLatch(3);
// 工作线程
public void worker() {
// 执行任务
latch.countDown(); // 计数器减1
}
// 主线程
public void mainThread() throws InterruptedException {
// 启动多个工作线程
latch.await(); // 等待计数器归零
// 继续执行后续操作
}
适用场景:
- 主线程等待多个子线程完成初始化
- 并行计算,等待所有计算完成
- 服务启动前的依赖检查
2. CyclicBarrier
让一组线程互相等待,到达公共屏障点。
特性:
- 可重复使用
- 可以设置屏障动作
- 基于ReentrantLock实现
使用方式:
// 创建屏障,指定参与线程数和屏障动作
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程已到达屏障");
});
public void worker() {
// 执行第一阶段工作
barrier.await(); // 等待其他线程
// 执行第二阶段工作
}
与CountDownLatch对比:
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
可重用性 | 不可重用 | 可重用 |
计数方向 | 递减 | 递增 |
等待线程 | 主线程等待 | 线程互相等待 |
屏障动作 | 不支持 | 支持 |
3. Semaphore
控制同时访问特定资源的线程数量。
特性:
- 基于AQS实现
- 支持公平和非公平模式
- 可动态调整许可数
使用方式:
// 创建信号量,指定许可数量
Semaphore semaphore = new Semaphore(5);
public void accessResource() throws InterruptedException {
semaphore.acquire(); // 获取许可
try {
// 访问受限资源
} finally {
semaphore.release(); // 释放许可
}
}
适用场景:
- 资源池管理
- 限流
- 有界集合
4. Exchanger
用于两个线程间交换数据。
特性:
- 线程成对交换
- 支持超时
- 适用于生产者-消费者模式
使用方式:
Exchanger<String> exchanger = new Exchanger<>();
// 线程1
String data1 = "Data from thread1";
String received1 = exchanger.exchange(data1);
// 线程2
String data2 = "Data from thread2";
String received2 = exchanger.exchange(data2);
适用场景:
- 管道传输
- 遗传算法
- 校对工作
5. Phaser
更灵活的可重用同步屏障。
特性:
- 动态注册/注销参与者
- 多阶段同步
- 支持分层结构
使用方式:
Phaser phaser = new Phaser(3); // 初始3个参与者
public void worker() {
// 阶段1工作
phaser.arriveAndAwaitAdvance(); // 到达并等待其他参与者
// 阶段2工作
phaser.arriveAndAwaitAdvance();
// 阶段3工作
phaser.arriveAndDeregister(); // 到达并注销
}
适用场景:
- 多阶段任务
- 动态线程池
- 复杂同步需求
五、并发集合类
虽然不是直接的同步工具,但也是并发编程重要部分:
1. ConcurrentHashMap
- 线程安全的HashMap实现
- 分段锁技术
- 高并发读性能
2. CopyOnWriteArrayList
- 写时复制
- 读操作无锁
- 适合读多写少场景
3. BlockingQueue
- 线程安全的队列
- 支持阻塞操作
- 实现包括:
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
SynchronousQueue
DelayQueue
4. ConcurrentSkipListMap/Set
- 基于跳表实现
- 有序的并发集合
- 无锁算法
六、同步工具选择指南
场景需求 | 推荐工具类 |
---|---|
简单同步 | synchronized |
复杂锁需求 | ReentrantLock |
读多写少 | ReadWriteLock |
线程安全计数器 | 原子类 |
主线程等待子线程完成 | CountDownLatch |
线程互相等待 | CyclicBarrier |
资源限制 | Semaphore |
线程间交换数据 | Exchanger |
复杂阶段同步 | Phaser |
并发集合 | ConcurrentHashMap等 |
七、最佳实践与注意事项
避免死锁:
- 按固定顺序获取多个锁
- 使用锁超时机制
- 避免在持有锁时调用外部方法
性能考虑:
- 减小同步块范围
- 读写分离
- 考虑无锁算法
避免过度同步:
- 只在必要时同步
- 优先使用不可变对象
- 考虑线程封闭技术
测试并发代码:
- 多线程压力测试
- 使用静态分析工具
- 考虑形式化验证
八、总结
Java 提供了丰富的同步工具类,从基础的 synchronized
和 volatile
,到灵活的 Lock
接口实现,再到高级的同步工具如 CountDownLatch
、CyclicBarrier
和 Phaser
,以及基于 CAS 的原子类。理解这些工具的特性和适用场景,可以帮助开发者编写出更高效、更安全的并发程序。
在选择同步工具时,应该根据具体需求考虑:
- 同步的粒度
- 性能要求
- 可维护性
- 复杂性
记住,没有万能的同步解决方案,每种工具都有其最适合的使用场景。合理选择和组合这些同步工具,才能构建出健壮的并发系统。