Java线程同步详解
在多线程编程中,线程同步是确保多个线程安全访问共享资源的核心机制。Java提供了多种同步工具和方法,以下是其核心实现方式及适用场景:
一、同步的核心目标
- 原子性:确保操作不可分割,避免中间状态被其他线程干扰。
- 可见性:确保共享变量的修改对其他线程立即可见。
- 有序性:防止指令重排序破坏程序逻辑。
二、同步的实现方式
1. 内置锁(synchronized)
• 原理:通过对象头的Monitor锁机制实现。 • 使用方式:
// 同步代码块
synchronized (lockObject) {
// 临界区代码
}
// 同步方法
public synchronized void method() {
// 临界区代码
}
• 特点: • 自动释放锁:代码块或方法执行完毕自动释放锁。 • 可重入性:同一线程可多次获取同一把锁。 • 性能开销:在JDK 6后优化(偏向锁、轻量级锁),适用于低竞争场景。
2. 显式锁(ReentrantLock)
• 原理:基于AQS(AbstractQueuedSynchronizer)实现,提供更灵活的锁控制。 • 使用方式:
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须手动释放锁
}
• 特点: • 可中断锁:lockInterruptibly()允许线程响应中断。 • 公平性:支持公平锁(按等待顺序获取锁)。 • 条件变量:通过Condition实现多条件等待。
3. volatile关键字
• 原理:通过内存屏障保证变量的可见性,禁止指令重排序。 • 使用方式:
private volatile boolean flag = false;
• 适用场景: • 单一变量的原子操作(如状态标志位)。 • 配合CAS操作实现无锁编程。
4. 原子类(AtomicInteger等)
• 原理:基于CAS(Compare-And-Swap)实现无锁原子操作。 • 使用方式:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子递增
• 适用场景: • 计数器、累加器等简单原子操作。 • 替代synchronized,减少锁竞争。
5. 同步工具类
• CountDownLatch:等待一组操作完成。
CountDownLatch latch = new CountDownLatch(3);
latch.await(); // 等待计数器归零
latch.countDown(); // 计数器减1
• CyclicBarrier:多个线程到达屏障后同时继续。
CyclicBarrier barrier = new CyclicBarrier(3);
barrier.await(); // 等待其他线程到达屏障
• Semaphore:控制并发线程数。
Semaphore semaphore = new Semaphore(5);
semaphore.acquire(); // 获取许可
semaphore.release(); // 释放许可
三、同步的核心问题与解决方案
1. 死锁
• 条件:互斥、占有且等待、不可抢占、循环等待。 • 预防: • 避免嵌套锁。 • 使用tryLock()设置超时时间。 • 按固定顺序获取锁。
2. 活锁
• 场景:线程不断重试失败的操作(如CAS自旋)。 • 解决:引入随机退避机制。
3. 饥饿
• 场景:低优先级线程长期无法获取资源。 • 解决:使用公平锁或调整线程优先级。
四、同步的性能优化
-
减小锁粒度 • 使用分段锁(如
ConcurrentHashMap的分段锁机制)。 • 避免锁住整个方法,仅同步必要代码块。 -
无锁编程 • 使用原子类(
AtomicInteger)或CAS操作。 • 示例:无锁计数器。public class NonblockingCounter { private AtomicInteger value = new AtomicInteger(0); public int getValue() { return value.get(); } public int increment() { return value.incrementAndGet(); } } -
读写分离 • 使用
ReentrantReadWriteLock,允许多个读线程共享锁。ReadWriteLock rwLock = new ReentrantReadWriteLock(); rwLock.readLock().lock(); // 读锁(共享) rwLock.writeLock().lock(); // 写锁(独占) -
锁粗化与锁消除 • 锁粗化:合并多个连续锁操作,减少锁开销。 • 锁消除:JVM通过逃逸分析移除不必要的锁。
五、同步的最佳实践
-
优先使用高层工具 • 使用
ConcurrentHashMap替代手动同步的HashMap。 • 使用BlockingQueue实现生产者-消费者模型。 -
避免嵌套锁 • 若必须使用多个锁,按固定顺序获取。
-
使用
tryLock()控制超时if (lock.tryLock(1, TimeUnit.SECONDS)) { try { /* 临界区 */ } finally { lock.unlock(); } } else { /* 处理超时 */ } -
优先使用不可变对象 • 不可变对象(如
String)无需同步。
六、示例场景
1. 线程安全的单例模式
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2. 生产者-消费者模型(BlockingQueue)
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产者
new Thread(() -> {
while (true) {
queue.put(1); // 队列满时自动阻塞
}
}).start();
// 消费者
new Thread(() -> {
while (true) {
Integer data = queue.take(); // 队列空时自动阻塞
}
}).start();
七、总结
| 同步方式 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
synchronized | 简单同步需求、低竞争环境 | 自动管理锁、代码简洁 | 灵活性较低 |
ReentrantLock | 复杂锁需求(如超时、公平性) | 支持条件变量、可中断锁 | 需手动释放锁 |
volatile | 单一状态标志、可见性保证 | 无锁、高性能 | 仅保证可见性,不保证复合操作原子性 |
| 原子类 | 计数器、累加器等简单原子操作 | 无锁、高性能 | 仅适用于单一变量 |
| 同步工具类 | 多线程协作(如任务分阶段) | 简化代码逻辑 | 功能特定 |
选择建议: • 简单场景:优先使用synchronized或原子类。 • 复杂需求:使用ReentrantLock或并发工具类。 • 性能敏感:采用无锁编程(CAS、不可变对象)。
通过合理选择同步机制,可以构建高效、安全的并发程序,同时避免死锁、活锁等常见问题。