一、并发编程导论:为什么需要并发?
大家好,我是你们这门课的老师。今天咱们聊聊并发编程最基础的问题——为什么我们需要它?
说白了,计算机发展这么多年,CPU 的速度越来越快,但内存、硬盘这些外围设备跟不上。你想想看,CPU 执行一条指令可能只要 0.3 纳秒,但去内存里读个数据要 100 纳秒。这中间的差距,就像你写代码只要 1 秒,但等编译器编译要 5 分钟。CPU 大部分时间都在「等」,多浪费啊。
我刚开始做后端开发时,遇到过一个线上问题:一个单线程的服务,处理一个请求要 200 毫秒,其中 180 毫秒都在等数据库返回。那时候我就想,要是能同时处理多个请求,把等待的时间利用起来,吞吐量不就上去了吗?
这就是并发的核心价值——让计算机在等待的时候别闲着。
并发 vs 并行:一字之差,天壤之别
这两个词很多人混着用,但它们的区别其实很关键。
| 概念 | 定义 | 打个比方 |
|---|---|---|
| 并发 | 多个任务在同一个时间段内交替执行 | 一个人同时烧水、切菜、煮饭——实际上是在切换任务 |
| 并行 | 多个任务在同一个时刻同时执行 | 三个人分别烧水、切菜、煮饭——真正同时干活 |
嗯,这里要注意:单核 CPU 只能实现并发,不能实现并行。只有多核 CPU 才能做到真正的并行。我见过不少面试者,一上来就说「并发就是同时做多件事」,其实不准确。并发是「看起来同时做」,并行才是「真的同时做」。
举个例子:你打开浏览器,一边下载文件,一边刷网页。如果 CPU 是单核的,那它其实是在下载和渲染之间快速切换,让你感觉是同时进行的。这就是并发。如果 CPU 是多核的,一个核专门下载,一个核专门渲染,那就是并行。
并发编程的四大挑战
并发虽好,但坑也多。我这些年踩过的坑,总结起来就是这四个:竞态条件、死锁、活锁、饥饿。
1. 竞态条件(Race Condition)
这是最常见的并发问题。说白了,就是多个线程同时访问共享数据,结果取决于谁先谁后,而这个顺序是不确定的。
我在项目中遇到过这样一个 bug:一个计数器,两个线程各加 100 万次,按理说结果应该是 200 万。但跑出来经常是 199 万 8 千多。为什么?
因为 count++ 这个操作不是原子的。它分三步:读 count、加 1、写回 count。两个线程同时读到了 100,各自加 1 后写回 101,结果只加了 1 次,而不是 2 次。
// 竞态条件示例
public class Counter {
private int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
}
2. 死锁(Deadlock)
死锁就是两个线程互相等待对方释放资源,结果谁也动不了。就像两个人过独木桥,你等我让,我等你让,结果都卡在那。
经典的死锁场景是这样的:线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1。两个线程就这么僵住了。
// 死锁示例
public class DeadlockDemo {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized(lockA) {
// 做一些事情
synchronized(lockB) {
// 做一些事情
}
}
}
public void method2() {
synchronized(lockB) {
// 做一些事情
synchronized(lockA) {
// 做一些事情
}
}
}
}
3. 活锁(Livelock)
活锁比死锁更隐蔽。线程没有阻塞,但一直在做无用功,就是无法推进。就像两个人迎面走来,都往左边让,结果还是面对面;又同时往右边让,还是面对面。反复让,就是过不去。
我记得有一次写一个重试机制,两个服务互相调用,A 调用 B 超时了,重试;B 也在重试调用 A。结果两个服务都在疯狂重试,CPU 跑满,但业务一点没推进。这就是活锁。
4. 饥饿(Starvation)
饥饿是指某个线程一直得不到所需的资源,无法执行。比如一个高优先级的线程总是抢占 CPU,低优先级的线程就一直等不到执行机会。
我遇到过的一个真实案例:一个日志写入线程,优先级设得很低。结果在高并发场景下,业务线程把 CPU 全占了,日志线程根本跑不起来。最后日志丢了,排查问题时才发现。嗯,从那以后我再也不敢随便调线程优先级了。
知识体系总览
下面这张图是我自己画的,把本章的核心知识点串起来了。你可以把它当作一个地图,后面每讲一个知识点,都能在这张图上找到位置。
这张图把本章内容分成了三大块:为什么需要并发、并发与并行的区别、四大挑战。后面几章我们会逐一深入每个挑战的解决方案,比如读写锁怎么解决竞态条件,无锁编程怎么避免死锁等等。
好了,这一章就到这里。并发编程不是一蹴而就的,但只要你理解了这些基础概念,后面的路就好走了。记住一句话:并发是手段,不是目的。不要为了用并发而用并发,先想清楚你的场景到底需不需要。