1. Java同步与互斥的核心概念
在多线程编程中,同步(Synchronization)和互斥(Mutual Exclusion)是保证线程安全的两个基本机制。当多个线程访问共享资源时,如果没有适当的控制,就会导致数据不一致和不可预测的行为。
同步指的是协调多个线程的执行顺序,确保它们按照预期的顺序访问共享资源。互斥则是确保同一时刻只有一个线程能够访问共享资源,防止数据竞争。
重要提示:在Java中,同步和互斥的实现都依赖于锁机制,但它们的关注点和使用场景有所不同。
1.1 为什么需要同步和互斥
假设我们有一个银行账户类,多个线程同时进行存取款操作:
java复制class BankAccount {
private int balance = 1000;
public void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
public void deposit(int amount) {
balance += amount;
}
}
如果没有同步控制,可能会出现以下问题:
- 两个取款线程同时检查余额都足够,然后都执行扣款,导致余额变为负数
- 一个存款线程和一个取款线程同时操作,可能导致最终余额计算错误
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java中的同步机制
2.1 synchronized关键字
synchronized是Java中最基本的同步机制,它可以用于方法或代码块:
java复制// 同步方法
public synchronized void synchronizedMethod() {
// 方法体
}
// 同步代码块
public void method() {
synchronized(this) {
// 同步代码
}
}
同步方法的锁是当前对象实例(this),而同步代码块可以指定任意对象作为锁。
2.1.1 同步方法的实现原理
当线程进入synchronized方法时:
- 尝试获取对象的监视器锁(monitor lock)
- 如果锁可用,线程获取锁并执行方法
- 如果锁被其他线程持有,当前线程进入阻塞状态
- 方法执行完成后自动释放锁
实际经验:同步方法比同步代码块粒度更粗,可能导致不必要的阻塞。在性能敏感的场景,建议使用更细粒度的同步代码块。
2.2 volatile关键字
volatile提供了一种轻量级的同步机制,它确保变量的可见性但不保证原子性:
java复制private volatile boolean flag = false;
volatile变量的特性:
- 可见性:对一个volatile变量的写操作对所有线程立即可见
- 禁止指令重排序:编译器不会对volatile变量的操作进行重排序
使用场景:适合作为状态标志位,但不适合用于需要原子性操作的场景。
3. Java中的互斥机制
3.1 ReentrantLock
ReentrantLock是Java 5引入的显式锁机制,比synchronized更灵活:
java复制private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
ReentrantLock的优势:
- 可中断的锁获取
- 超时获取锁
- 公平锁与非公平锁选择
- 可以绑定多个条件(Condition)
3.1.1 ReentrantLock vs synchronized
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 锁获取方式 | 显式 | 隐式 |
| 可中断 | 支持 | 不支持 |
| 超时获取 | 支持 | 不支持 |
| 公平锁 | 可配置 | 非公平 |
| 条件变量 | 支持多个 | 单个 |
| 性能 | Java 5-6更快 | Java 6+优化后相当 |
3.2 ReadWriteLock
对于读多写少的场景,ReentrantReadWriteLock可以提高并发性能:
java复制private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public void read() {
readLock.lock();
try {
// 读操作
} finally {
readLock.unlock();
}
}
public void write() {
writeLock.lock();
try {
// 写操作
} finally {
writeLock.unlock();
}
}
4. 高级同步工具类
4.1 CountDownLatch
允许一个或多个线程等待其他线程完成操作:
java复制CountDownLatch latch = new CountDownLatch(3);
// 工作线程
new Thread(() -> {
// 执行任务
latch.countDown();
}).start();
// 等待线程
latch.await(); // 阻塞直到计数器为0
4.2 CyclicBarrier
让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障:
java复制CyclicBarrier barrier = new CyclicBarrier(3, () -> {
// 所有线程到达后执行的任务
});
new Thread(() -> {
// 执行任务
barrier.await(); // 等待其他线程
}).start();
4.3 Semaphore
控制同时访问特定资源的线程数量:
java复制Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问
semaphore.acquire(); // 获取许可
try {
// 使用资源
} finally {
semaphore.release(); // 释放许可
}
5. 原子变量类
Java的java.util.concurrent.atomic包提供了一系列原子变量类,如AtomicInteger、AtomicLong等:
java复制private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}
原子类的实现原理:
- 基于CAS(Compare-And-Swap)操作
- 底层使用Unsafe类直接操作内存
- 适合简单的原子操作,性能优于锁
注意事项:虽然原子类性能好,但不适合复杂的复合操作。对于需要多个变量保持一致的场景,仍需使用锁。
6. 线程安全集合
Java提供了多种线程安全的集合类:
6.1 ConcurrentHashMap
高并发的HashMap实现:
java复制ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
实现特点:
- 分段锁技术(Java 7)或CAS+synchronized(Java 8+)
- 高并发读写性能
- 弱一致性迭代器
6.2 CopyOnWriteArrayList
写时复制的List实现:
java复制CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("item");
特点:
- 读操作无锁,性能极高
- 写操作复制整个数组,适合读多写少场景
- 迭代器反映创建时的状态
7. 死锁与避免
7.1 死锁产生的四个必要条件
- 互斥条件:资源一次只能由一个线程占用
- 占有且等待:线程持有资源并等待其他资源
- 不可抢占:已分配的资源不能被其他线程强行夺取
- 循环等待:存在一个线程等待环路
7.2 死锁避免策略
- 加锁顺序:所有线程按相同顺序获取锁
- 加锁时限:尝试获取锁超时则放弃
- 死锁检测:定期检查死锁并恢复
示例代码演示如何避免死锁:
java复制// 定义全局锁顺序
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public void method1() {
synchronized(lock1) {
synchronized(lock2) {
// 操作
}
}
}
public void method2() {
synchronized(lock1) { // 与method1相同的顺序
synchronized(lock2) {
// 操作
}
}
}
8. 性能优化建议
- 减小同步范围:只在必要的地方同步
- 降低锁粒度:使用更细粒度的锁
- 读写分离:读操作使用读锁,写操作使用写锁
- 避免锁嵌套:容易导致死锁
- 考虑无锁算法:如原子变量类
- 使用并发集合:代替手动同步的集合
性能测试经验:在Java 8+中,synchronized经过优化后性能与ReentrantLock相当,代码更简洁时优先考虑synchronized。
9. 常见问题排查
9.1 线程阻塞分析
使用jstack工具分析线程堆栈:
bash复制jstack <pid>
查找BLOCKED状态的线程和持有的锁信息。
9.2 死锁检测
jstack输出中会明确标识死锁:
code复制Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f88e4003c58 (object 0x000000076ab270c0, a java.lang.Object),
which is held by "Thread-0"
9.3 锁竞争优化
使用JVisualVM或Java Mission Control监控锁竞争情况,优化热点锁。
10. Java内存模型(JMM)与同步
Java内存模型定义了线程如何与内存交互,以及同步操作如何影响内存可见性。
10.1 happens-before规则
重要的happens-before关系:
- 程序顺序规则
- 监视器锁规则
- volatile变量规则
- 线程启动规则
- 线程终止规则
10.2 同步的内存语义
锁的获取和释放会建立happens-before关系,确保内存可见性:
- 释放锁前对共享变量的修改对后续获取该锁的线程可见
- 获取锁会强制刷新处理器缓存
在实际开发中,理解这些底层原理有助于编写正确高效的多线程代码。我个人的经验是,对于大多数应用场景,优先考虑使用高层并发工具类(如ConcurrentHashMap)而不是手动同步,它们通常经过充分优化且更不容易出错。只有在性能关键路径或特殊需求时,才考虑使用底层同步机制。
