1. 为什么我们需要并发编程?
在单核CPU时代,程序执行就像一个人在厨房里做饭——你只能先切菜、再炒菜、最后装盘,所有步骤必须顺序执行。但现代计算机早已进入多核时代,就像拥有多位厨师的厨房,可以同时进行多项任务。这种能力就是并发(Concurrency)的核心价值。
我曾在处理一个电商促销系统时,最初采用单线程处理订单,结果每秒只能处理20单,活动开始后系统立即崩溃。改用多线程后,同样的硬件可以处理2000+ TPS。这个经历让我深刻理解到:并发不是可选技能,而是现代开发的必备能力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线程:并发的基本执行单元
2.1 线程的本质与实现
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中。用快递仓库来比喻:
- 进程就像整个仓库(拥有独立地址空间)
- 线程就像仓库里的分拣工人(共享进程资源)
在Java中创建一个线程有两种基本方式:
java复制// 方式1:继承Thread类
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
// 方式2:实现Runnable接口
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable running");
}
}
实际开发中更推荐实现Runnable接口,因为Java不支持多重继承,这种方式更灵活。
2.2 线程的生命周期与状态转换
线程从创建到销毁会经历多个状态:
- NEW:刚创建未启动
- RUNNABLE:可运行状态(可能在运行也可能在等待CPU)
- BLOCKED:等待监视器锁(同步代码块)
- WAITING:无限期等待(Object.wait())
- TIMED_WAITING:限期等待(Thread.sleep())
- TERMINATED:执行结束
我曾遇到一个线上问题:线程卡在BLOCKED状态导致系统假死。通过jstack获取线程dump后发现,是因为两个线程互相等待对方持有的锁,形成了经典的死锁(Deadlock)。
3. 等待通知机制详解
3.1 生产者-消费者模式实战
等待通知机制最典型的应用就是生产者-消费者模式。下面是一个使用Object的wait/notify实现的例子:
java复制class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private int maxSize = 10;
public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == maxSize) {
wait(); // 缓冲区满,生产者等待
}
queue.add(value);
notifyAll(); // 通知消费者
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 缓冲区空,消费者等待
}
int value = queue.poll();
notifyAll(); // 通知生产者
return value;
}
}
注意这里必须用while循环检查条件,不能用if。因为线程被唤醒后条件可能已经改变(虚假唤醒问题)。
3.2 Condition接口的高级用法
Java 5之后提供了更灵活的Condition接口,它能够实现更精细的线程控制。比如我们可以创建多个条件队列:
java复制class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 类似的take方法...
}
这种实现方式比传统的wait/notify更灵活,可以精确控制唤醒哪些线程。
4. 并发编程的常见陷阱与解决方案
4.1 线程安全问题的本质
线程安全问题的根源在于共享数据的非原子性访问。比如经典的i++问题:
java复制// 看似简单的操作实际上包含三个步骤:
// 1. 读取i的值
// 2. 将值加1
// 3. 写回新值
在多线程环境下,这三个步骤可能被其他线程打断,导致最终结果不符合预期。
4.2 同步机制的选型指南
解决线程安全问题有多种方案,各有适用场景:
| 方案 | 适用场景 | 性能影响 | 示例 |
|---|---|---|---|
| synchronized | 简单同步块 | 中等 | 方法级同步 |
| ReentrantLock | 需要高级功能 | 较高 | 可中断锁 |
| volatile | 可见性保证 | 低 | 状态标志 |
| Atomic类 | 简单原子操作 | 最低 | 计数器 |
| ThreadLocal | 线程隔离数据 | 无竞争 | 用户会话 |
我在一个高频交易系统中,将synchronized替换为AtomicLong后,吞吐量提升了3倍。但要注意Atomic类只适合简单场景,复杂逻辑仍需锁机制。
4.3 死锁预防的四个必要条件
死锁发生的四个必要条件(必须全部满足):
- 互斥条件:资源一次只能一个线程占用
- 占有且等待:持有资源并等待其他资源
- 不可抢占:资源只能自愿释放
- 循环等待:存在线程循环等待链
预防死锁的实用技巧:
- 按固定顺序获取锁(所有线程按相同顺序)
- 使用tryLock()设置超时
- 通过jstack定期检查死锁
5. 现代并发工具类实战
5.1 线程池的最佳实践
创建线程池的正确方式:
java复制// 不推荐直接new ThreadPoolExecutor
ExecutorService executor = Executors.newFixedThreadPool(4);
// 更推荐自定义参数
ThreadPoolExecutor customExecutor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
实际项目中,我通常根据任务类型选择线程池参数:
- CPU密集型:线程数 = CPU核心数 + 1
- IO密集型:线程数 = CPU核心数 * (1 + 平均等待时间/平均计算时间)
5.2 Fork/Join框架深度解析
Fork/Join是Java 7引入的并行任务框架,特别适合分治算法。比如计算斐波那契数列:
java复制class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) { this.n = n; }
protected Integer compute() {
if (n <= 1) return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork(); // 异步执行
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join(); // 等待结果
}
}
关键点:
- fork():异步执行子任务
- join():获取子任务结果
- 使用工作窃取(Work-Stealing)算法提高效率
6. 高并发系统设计经验谈
6.1 并发编程的性能优化
在高并发场景下,一些细微的优化可能带来显著提升:
- 减少锁粒度:从方法级同步改为代码块同步
- 读写分离:使用ReadWriteLock替代完全同步
- 无锁编程:尝试Atomic类和CAS操作
- 对象池化:避免频繁创建销毁对象
我曾优化过一个日志服务,将synchronized改为ReentrantReadWriteLock后,QPS从500提升到5000。
6.2 并发测试的实用技巧
使用JMeter进行并发测试时要注意:
- 逐步增加并发用户数(ramp-up period)
- 合理设置思考时间(think time)
- 监控系统资源(CPU、内存、IO)
- 使用分布式测试应对超高并发
一个常见的误区是直接在本地机器上模拟高并发,这会导致测试结果失真。正确的做法是使用多台测试机分布式执行。
7. 从理论到实践:完整案例解析
让我们通过一个完整的订单处理系统案例,串联所有知识点:
java复制class OrderSystem {
private final ExecutorService executor;
private final BlockingQueue<Order> queue;
private final AtomicInteger counter = new AtomicInteger();
public OrderSystem(int poolSize) {
this.executor = Executors.newFixedThreadPool(poolSize);
this.queue = new LinkedBlockingQueue<>(1000);
startConsumers();
}
private void startConsumers() {
for (int i = 0; i < 3; i++) {
executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Order order = queue.take(); // 阻塞获取
processOrder(order);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
}
public void submitOrder(Order order) {
if (!queue.offer(order)) {
throw new IllegalStateException("Order queue full");
}
counter.incrementAndGet();
}
private void processOrder(Order order) {
// 实际处理逻辑...
}
}
这个实现包含了:
- 线程池管理
- 阻塞队列应用
- 原子计数器
- 优雅的线程中断处理
- 流量控制(队列满时拒绝)
在实际项目中,我还添加了监控指标和熔断机制,当处理失败率达到阈值时自动降级。
