一、 问题的根源:竞态条件(Race Condition)
当多个线程同时访问和修改同一共享资源时,最终的执行结果依赖于线程执行的精确时序。这种不确定性往往会导致错误的发生,这就是竞态条件。
示例:一个不安全的计数器
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这并非原子操作
}
public int getCount() {
return count;
}
}
count++这行代码看似简单,但实际上是一个“读取-修改-写入”的复合操作。如果两个线程同时读取到相同的值,分别增加后写入,就会导致一次增加被丢失,最终结果会小于预期。
二、 同步的核心武器:synchronized
Java提供了内置的synchronized关键字来实现同步,它是一种互斥锁,确保同一时刻最多只有一个线程可以执行某个方法或代码块。
1. 同步方法(Synchronized Methods)
public class SynchronizedCounter {
private int count = 0;
// 同步方法,锁是当前实例对象(this)
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
原理:线程在进入同步方法前必须获得当前对象实例(this)的锁。如果锁被其他线程持有,当前线程将被阻塞,直到锁被释放。
2. 同步代码块(Synchronized Blocks)
同步方法会锁住整个方法,有时粒度太粗,影响性能。同步代码块可以更精细地控制锁的范围,并允许选择不同的锁对象(监视器)。
public class FineGrainedSynchronization {
private final Object lock = new Object(); // 专门的锁对象
private int count = 0;
public void increment() {
// 只同步关键的代码部分,性能更好
synchronized (lock) {
count++;
}
// 这里可以执行其他非线程安全的操作
}
}
使用一个专门的私有锁对象(private final Object lock)是更好的实践,它可以防止外部代码意外获取你的锁,导致死锁。
三、 更现代的选择:Lock接口
Java 5引入了java.util.concurrent.locks包,提供了更灵活、更强大的锁操作,主要是ReentrantLock。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 手动获取锁
try {
count++;
} finally {
lock.unlock(); // 必须在finally块中确保释放锁
}
}
}
相比于synchronized的优势:
- 尝试非阻塞获取锁:
tryLock()方法可以立即返回或在一定时间内尝试获取锁,避免无限期阻塞。 - 可中断的锁获取:
lockInterruptibly()方法允许在等待锁时响应中断。 - 公平锁:
ReentrantLock可以设置为公平锁(先等待的线程先获得锁),虽然可能会降低吞吐量。
四、 实战示例:生产者-消费者模型
这是一个经典的线程同步问题,完美展示了wait(), notify(), notifyAll()(通常与synchronized配合)或Condition(与Lock配合)的使用。
使用Lock和Condition实现:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerDemo {
private final Queue<Integer> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列未满条件
private final Condition notEmpty = lock.newCondition(); // 队列非空条件
public ProducerConsumerDemo(int capacity) {
this.capacity = capacity;
}
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列已满,生产者等待
}
queue.add(value);
System.out.println("Produced: " + value);
notEmpty.signalAll(); // 生产后通知消费者可以消费了
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列为空,消费者等待
}
int value = queue.poll();
System.out.println("Consumed: " + value);
notFull.signalAll(); // 消费后通知生产者可以生产了
} finally {
lock.unlock();
}
}
}
这个例子展示了如何利用多个Condition对象精确地唤醒特定类型的线程(只唤醒生产者或只唤醒消费者),比使用synchronized的notifyAll()更高效。
五、 总结与选型建议
- synchronized:优先考虑。语法简单,JVM会自动释放锁,不易出错。在大多数情况下性能已经足够好(JVM对其有大量优化,如锁升级)。
- Lock:当你需要尝试获取锁、可中断、公平锁等高级功能时使用。但要切记手动在
finally中释放锁。
核心思想:同步的本质是缩小临界区(被锁保护的代码范围),在保证线程安全的前提下,最大限度地提高程序的并发性能和吞吐量。选择最适合业务场景的同步工具,是构建高效、稳定并发应用的关键。

被折叠的 条评论
为什么被折叠?



