
Java多线程同步:从理论到实战,告别并发“翻车”现场
大家好,作为一名在Java世界里摸爬滚打多年的开发者,我敢说,多线程编程是区分“码农”和“工程师”的一道重要分水岭。它能让你的程序性能飞升,但也可能带来一堆令人抓狂的Bug:数据错乱、死锁、性能瓶颈……这些“翻车”现场,我几乎都经历过。今天,我们就来深入聊聊Java中的多线程同步机制,这不仅是面试高频考点,更是构建稳定、高效并发程序的基石。我会结合自己的实战经验,带你理解原理,避开那些年我踩过的坑。
一、为什么需要同步?一个经典的“翻车”案例
在深入技术细节前,我们先看一个几乎所有Java开发者都写过的“反面教材”——一个简单的计数器。假设我们有一个共享的计数器,多个线程同时对其进行累加。
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这行代码是“危险”的!
}
public int getCount() {
return count;
}
}
然后我们启动10个线程,每个线程对这个计数器加1000次。理论上,最终结果应该是10000,对吧?但实际运行多次,你很可能得到像 9987, 9992 这样小于10000的结果。这就是典型的竞态条件(Race Condition)。
踩坑提示: `count++` 这行看似原子的操作,在JVM底层实际是“读取-修改-写入”三步。当多个线程交叉执行这三步时,就会发生更新丢失。我第一次遇到时,排查了半天才锁定是这里的问题。
二、核心同步武器库:synchronized 关键字
Java中最基础、最常用的同步工具就是 `synchronized` 关键字。它可以用来修饰方法或代码块。
1. 同步实例方法
锁住的是当前对象实例(`this`)。
public class SafeCounter {
private int count = 0;
// 同步方法,锁是当前SafeCounter实例
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
现在再运行测试,结果稳稳的是10000。`synchronized` 保证了同一时刻,只有一个线程能进入某个对象的同步方法。
2. 同步静态方法
锁住的是当前类的 `Class` 对象。这意味着它锁的是整个类,所有实例都共享这把锁。
public class Logger {
// 静态同步方法,锁是Logger.class
public static synchronized void log(String msg) {
// 写入日志文件,需要全局同步
System.out.println(Thread.currentThread().getName() + ": " + msg);
}
}
3. 同步代码块
这是更灵活的方式,可以指定任意的对象作为“锁”(监视器)。
public class FineGrainedLock {
private final Object lockA = new Object();
private final Object lockB = new Object();
private int dataA = 0;
private int dataB = 0;
public void updateA() {
synchronized (lockA) { // 只锁与dataA相关的部分
dataA++;
// 一些耗时操作...
}
}
public void updateB() {
synchronized (lockB) { // 只锁与dataB相关的部分
dataB++;
// 一些耗时操作...
}
}
}
实战经验: 使用专门的私有对象(如 `private final Object lock = new Object();`)作为锁,而不是锁 `this` 或字符串常量,可以避免外部代码意外获取你的锁导致死锁,也提高了代码的清晰度和可控性。这是我早期从代码审查中学到的重要一课。
三、更强大的工具:java.util.concurrent.locks 包
`synchronized` 简单易用,但功能有限(比如无法中断等待、无法设置超时、非公平锁)。在复杂的并发场景下,`ReentrantLock` 等显示锁提供了更精细的控制。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private int count = 0;
private final Lock lock = new ReentrantLock(); // 可重入锁
public void increment() {
lock.lock(); // 手动获取锁
try {
count++;
// 其他需要同步的操作...
} finally {
lock.unlock(); // 必须在finally块中释放锁!
}
}
// 尝试获取锁,避免死锁的常用模式
public boolean tryIncrement() {
if (lock.tryLock()) { // 尝试获取锁,立即返回成功与否
try {
count++;
return true;
} finally {
lock.unlock();
}
} else {
// 获取锁失败,执行其他逻辑(如记录日志、重试策略)
return false;
}
}
// 带超时的尝试
public boolean incrementWithTimeout() throws InterruptedException {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 等待最多1秒
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
}
踩坑提示: 使用 `ReentrantLock` 时,务必在 `finally` 块中调用 `unlock()`!否则,如果同步代码块中发生异常,锁将永远不会被释放,导致灾难性的死锁。我曾在生产环境因为一个未捕获的 `RuntimeException` 而忘了释放锁,导致服务线程全部挂起,教训深刻。
四、协调与通信:wait(), notify() 与 Condition
有时线程间需要协作,比如生产者-消费者模型。`Object` 类的 `wait()`, `notify()`, `notifyAll()` 是基础工具,但它们必须用在 `synchronized` 块内部。
public class SimpleBlockingQueue {
private final Queue queue = new LinkedList();
private final int capacity;
public SimpleBlockingQueue(int capacity) {
this.capacity = capacity;
}
// 生产者方法
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // 队列满,生产者等待
}
queue.add(item);
notifyAll(); // 通知可能正在等待的消费者
}
// 消费者方法
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 队列空,消费者等待
}
T item = queue.poll();
notifyAll(); // 通知可能正在等待的生产者
return item;
}
}
重要原则: 永远在循环中检查条件并调用 `wait()`,而不是用 `if`。因为被唤醒时,条件可能仍未满足(“虚假唤醒”)。这是我早期常犯的错误。
对于 `ReentrantLock`,可以使用与之绑定的 `Condition` 对象,它提供了更清晰的线程分组等待能力(如分开等待“非满”和“非空”条件)。
五、实战选型与性能考量
那么,到底该用 `synchronized` 还是 `ReentrantLock` 呢?
- 优先使用 `synchronized`: 对于大多数常规同步需求,它简洁、自动释放锁、JVM持续优化(如锁升级:偏向锁->轻量级锁->重量级锁),性能已经不输甚至在某些场景优于显示锁。
- 考虑 `ReentrantLock`: 当你需要可定时的锁等待、可中断的锁等待、公平锁、或者需要绑定多个Condition时。例如,实现一个带有超时机制的连接池。
最后,终极建议: 在Java 5之后,许多通用并发模式(如线程池、并发集合、信号量、计数器)都已经在 `java.util.concurrent` 包中有了高质量的实现(如 `ConcurrentHashMap`, `CountDownLatch`, `CyclicBarrier`)。在解决实际问题时,“站在巨人的肩膀上”,优先考虑使用这些高级并发工具,而不是自己从头用锁去造轮子。这能极大降低出错概率,提升开发效率。这是我多年实践后最深刻的体会。
希望这篇结合了原理、代码和血泪经验的指南,能帮助你更好地驾驭Java多线程同步,写出既快又稳的并发程序。记住,理解原理,谨慎使用,善用工具,你就能远离那些令人头疼的并发“翻车”现场。

评论(0)