并发的优势与挑战
天下没有免费的午餐,并发带来的性能提升是有代价的。
先看一个场景
凌晨 2 点,你的电商系统突然涌入大量用户。服务器只有 4 核 CPU。
不用并发:用户排队等待,一个请求处理完再处理下一个。用户等到超时。
用并发:多个请求同时处理,CPU 切换处理。用户响应快,服务器资源被充分利用。
这就是并发的价值——让硬件资源物尽其用。
并发的优势
1. 资源利用率:CPU 不再空等
不用并发:
CPU: ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
↑ 处理请求 ↑ 等待数据库响应(CPU 空转)...
用并发:
CPU: ██████████████████████████████████████████...
↑ 处理请求1 ↑ 处理请求2 ↑ 处理请求3代码演示:
java
public class ResourceUtilization {
public static void main(String[] args) throws InterruptedException {
long start = System.nanoTime();
// 假设有 3 个 I/O 任务,每个需要 1 秒
// 串行:3 秒
// 并发:约 1 秒
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(() -> task("文件1", 1000));
executor.submit(() -> task("文件2", 1000));
executor.submit(() -> task("文件3", 1000));
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("并发耗时: " + duration + "ms");
}
private static void task(String name, int ms) {
try {
System.out.println(name + " 开始");
Thread.sleep(ms);
System.out.println(name + " 完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}2. 响应速度:后台任务不阻塞用户
java
public class ResponsivenessDemo {
public static void main(String[] args) {
System.out.println("开始处理请求");
// 耗时操作放后台线程,不阻塞主线程
new Thread(() -> {
// 模拟耗时任务
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("后台任务完成");
}, "后台任务").start();
// 主线程立即返回
System.out.println("请求处理完成(后台任务继续运行)");
}
}3. 任务分解:复杂问题简单化
java
public class TaskDecomposition {
public static void main(String[] args) {
// 假设要处理一个大文件
// 不用并发:从头到尾处理,耗时 T
// 用并发:分成 4 份,4 个线程同时处理,耗时 T/4
String bigFile = "huge_data.csv";
int threadCount = 4;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
// 分成 4 段处理
long segmentSize = getFileSize(bigFile) / threadCount;
for (int i = 0; i < threadCount; i++) {
long start = i * segmentSize;
long end = (i == threadCount - 1) ? getFileSize(bigFile) : (i + 1) * segmentSize;
executor.submit(() -> processSegment(bigFile, start, end));
}
executor.shutdown();
}
private static long getFileSize(String file) {
return 1_000_000_000L; // 模拟
}
private static void processSegment(String file, long start, long end) {
// 处理文件段
}
}4. 硬件利用率:充分利用多核
java
public class MultiCoreUtilization {
public static void main(String[] args) {
int cores = Runtime.getRuntime().availableProcessors();
System.out.println("CPU 核心数: " + cores);
// CPU 密集型任务
ExecutorService executor = Executors.newFixedThreadPool(cores);
for (int i = 0; i < cores; i++) {
executor.submit(() -> {
// 每个线程占用一个 CPU 核心
long sum = 0;
for (int j = 0; j < 1_000_000_000; j++) {
sum += j;
}
});
}
executor.shutdown();
}
}并发的挑战
1. 线程安全:共享数据的一致性
这是并发最核心的问题。
java
public class RaceConditionDemo {
private int counter = 0; // 共享变量
public void increment() {
counter++; // ❌ 不是原子操作!
}
public static void main(String[] args) throws InterruptedException {
RaceConditionDemo demo = new RaceConditionDemo();
// 1000 个线程,每个累加 1000 次
// 理论上结果应该是 1000000
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(5000); // 等待所有线程完成
System.out.println("预期: 1000000, 实际: " + demo.counter);
// 结果可能小于 1000000,因为 counter++ 不是原子的
}
}为什么 counter++ 不是原子的?
counter++ 实际分三步:
1. 从内存读取 counter 到 CPU 寄存器
2. 在 CPU 中执行 +1 操作
3. 把结果写回内存
线程 A:读取 counter=0 → 计算 1 → 还没写回
线程 B:读取 counter=0 → 计算 1 → 写回 counter=1
线程 A:写回 counter=1 → 结果:counter=1(丢失了线程 B 的更新)2. 死锁:互相等待对方释放锁
java
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 线程 1:先锁 A,再锁 B
new Thread(() -> {
synchronized (lockA) {
System.out.println("线程1: 持有锁A");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("线程1: 持有锁B");
}
}
}, "Thread-1").start();
// 线程 2:先锁 B,再锁 A
new Thread(() -> {
synchronized (lockB) {
System.out.println("线程2: 持有锁B");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("线程2: 持有锁A");
}
}
}, "Thread-2").start();
// 死锁!线程1等线程2释放B,线程2等线程1释放A
}
}3. 可见性问题:一个线程的修改,另一个线程看不到
java
public class VisibilityDemo {
private static boolean ready = false;
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!ready) {
// 理论上,ready 变成 true 时,循环应该退出
// 但由于可见性问题,可能永远看不到 ready=true
}
System.out.println(number); // 可能输出 0
}).start();
Thread.sleep(100); // 确保上面的线程先启动
new Thread(() -> {
number = 42;
ready = true; // 修改后,其他线程可能看不到
}).start();
}
}4. 活锁:线程一直在运行,但工作没有进展
java
public class LivelockDemo {
// 两个线程都在运行,但都在礼让对方
public static void main(String[] args) {
AtomicBoolean aliceTurn = new AtomicBoolean(true);
AtomicBoolean bobTurn = new AtomicBoolean(false);
// Alice
new Thread(() -> {
while (true) {
if (aliceTurn.compareAndSet(true, false)) {
System.out.println("Alice 完成任务");
bobTurn.set(true);
}
// 模拟处理时间很短,可能导致不断切换
Thread.yield();
}
}).start();
// Bob
new Thread(() -> {
while (true) {
if (bobTurn.compareAndSet(true, false)) {
System.out.println("Bob 完成任务");
aliceTurn.set(true);
}
Thread.yield();
}
}).start();
}
}优势 vs 挑战对比
| 场景 | 不用并发 | 用并发 |
|---|---|---|
| CPU 利用率 | 低(单核) | 高(多核) |
| I/O 等待 | 阻塞 | 切换执行其他任务 |
| 响应时间 | 长 | 短(后台任务不阻塞) |
| 吞吐量 | 低 | 高 |
| 开发复杂度 | 简单 | 复杂 |
| 调试难度 | 低 | 高 |
| 数据安全 | 无问题 | 需要同步 |
何时用并发?
适合并发的场景:
- I/O 密集型(等待时间长)
- 需要并行计算(多核 CPU)
- 需要高吞吐量
- 需要快速响应(后台任务不阻塞)
不适合并发的场景:
- 任务太短(创建线程开销 > 任务本身)
- 任务之间有强依赖(无法并行)
- 单核 CPU 且无 I/O(并发反而增加开销)
总结
并发是双刃剑:
- 优势:充分利用硬件、提高响应速度、提升吞吐量
- 挑战:线程安全、死锁、可见性、活锁
关键原则:
- 不要过度并发
- 避免不必要的共享
- 使用不可变对象
- 选择合适的同步机制
- 优先使用高级抽象(线程池、并发工具类)
下一节我们深入探讨线程安全问题。
