并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课

并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课

「Java 进阶之路」系列 Day01

写在前面

如果你去面试,被问到"什么是并发,什么是并行",十有八九会脱口而出:"并发是同时处理多个任务,并行是同时执行多个任务"------然后被追问一句"那到底有什么区别"就卡壳了。

这篇文章不讲虚的,用一张图 + 一个类比 + 一段能跑的代码,把这个"看似简单实则容易讲不清"的基础问题彻底讲透,同时把线程的生命周期也一起捋顺,作为整个并发系列的第一篇。


一、是什么:并发不等于并行

先说结论:

并发(Concurrency) :CPU 在多个任务之间快速切换,看起来像"同时进行",但某一时刻可能只有一个任务在真正执行。 并行(Parallelism):多核 CPU 上,多个任务真正地在同一时刻执行。

一个经典类比:

  • 你一个人(单核 CPU)一边接电话一边写代码,看起来是"同时"在做两件事,其实是电话响了就切过去接,挂了再切回来写代码------这是并发
  • 你和同事两个人(多核 CPU),一个人打电话,另一个人写代码,两件事真正同时发生------这是并行

放到 Java 里:单核机器上开 100 个线程,操作系统靠时间片轮转让它们"看起来"在同时跑,这是并发;4 核机器上 4 个线程分别跑在不同核心上,才是真正的并行。并发是一种编程模型,并行是一种执行状态,并发程序不一定并行执行,但并行执行一定是并发的一种表现形式。

为什么要引入并发

一句话理解并发编程的本质:让多个任务"同时推进",提高资源利用率

优点 说明
提升性能(最核心) 线程在等 IO 的时候,CPU 可以先去处理别的任务,不用干等着
提高吞吐量(TPS/QPS) 单位时间能处理更多请求
更好地利用资源 IO 等待期间 CPU 不被浪费
提升用户体验 耗时操作不阻塞主线程(比如 Android 主线程、Web 请求线程)

但并发不是免费的午餐:

缺点 说明
代码复杂度暴增 执行顺序不确定,难以理解和维护
线程安全问题 竞态条件、数据不一致、覆盖写
死锁 两个线程互相等待对方持有的锁,永久卡死
调试困难 并发 bug 往往是"线上偶现、本地必现不了"
性能不一定提升 线程切换、锁竞争本身也有开销
资源消耗增加 线程不是免费的,每个线程默认占用 1MB 左右的栈内存

记住这句话:并发解决的是"等待问题"(IO 等待、资源等待),不是"计算问题"。如果任务本身是纯 CPU 密集型计算,堆再多线程也不会让单核跑得更快------这时候需要的是并行(多核),而不是并发(单核切换)。


二、线程的生命周期:6 种状态到底怎么流转

理解了并发的本质,再来看承载并发的最小单位------线程,它一生要经历 6 种状态:

状态 触发条件 说明
NEW 创建线程对象,未调用 start() 不占 CPU,还没进入调度
RUNNABLE 调用 start() 有资格运行,但不代表正在运行,可能在排队等 CPU 时间片
BLOCKED 等待 synchronized 抢同一把锁没抢到,进入此状态
WAITING 调用 wait()join() 主动等待,不占 CPU,必须被显式唤醒
TIMED_WAITING 调用 sleep(t)wait(t)join(t) 等待指定时间后自动醒来
TERMINATED 线程执行完毕 终态,不可再次 start()

流转关系画成图:

stateDiagram-v2 [*] --> NEW NEW --> RUNNABLE: start() RUNNABLE --> TERMINATED: 执行完毕 RUNNABLE --> BLOCKED: synchronized 抢锁失败 BLOCKED --> RUNNABLE: 拿到锁 RUNNABLE --> WAITING: wait() WAITING --> RUNNABLE: notify() RUNNABLE --> TIMED_WAITING: sleep(t) TIMED_WAITING --> RUNNABLE: 时间到 TERMINATED --> [*]

一段代码看清 NEW → RUNNABLE → TERMINATED

java 复制代码
public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000); // 进入 TIMED_WAITING
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        System.out.println("刚 new 完:" + t.getState());   // NEW

        t.start();
        System.out.println("start() 之后:" + t.getState()); // RUNNABLE

        Thread.sleep(200); // 留点时间让子线程进入 sleep
        System.out.println("子线程 sleep 中:" + t.getState()); // TIMED_WAITING

        t.join(); // 主线程等待子线程结束
        System.out.println("执行完毕:" + t.getState());     // TERMINATED
    }
}

两个最容易混淆的方法:sleep vs wait

这是面试高频送分题,但很多人答不全:

对比项 sleep() wait()
所属类 Thread 的静态方法 Object 的实例方法
是否释放锁 不释放 释放
使用限制 任何地方都能用 必须在 synchronized 同步块/方法内使用,否则抛 IllegalMonitorStateException
唤醒方式 时间到自动醒 时间到(wait(t))或被 notify()/notifyAll() 唤醒

一句话记忆:sleep 是"占着位置不干活",wait 是"让出位置去排队" 。这也是为什么生产者-消费者模型必须用 wait/notify 而不是 sleep------用 sleep 的话线程会一直攥着锁不放,别的线程根本没机会拿到锁去改变条件。


三、排查技巧:从线程状态反推问题现象

这部分是纯经验,建议直接背下来,线上排查故障时能救命:

现象 可能原因
接口卡死、请求没响应 大概率有线程卡在 WAITINGBLOCKED 状态(锁没释放、下游一直没返回)
CPU 飙高 大量线程处于 RUNNABLE 状态,频繁抢占 CPU、上下文切换开销大
程序迟迟不退出 有线程没到 TERMINATED,可能是死循环,或者卡在某个阻塞调用上

排查手段:拿到 CPU 占用最高的线程 ID(top -Hp <pid>),转成 16 进制后去线程 dump(jstack)里搜对应的 nid,看它当时的状态和堆栈,基本就能定位到问题代码行。


四、面试追问

Q1:并发和并行的区别,能不能用一个生活化的例子讲清楚?

并发是单核靠时间片切换"看起来同时",并行是多核"真正同时"。类比:一个人一边接电话一边写代码是并发(来回切换);两个人分别打电话、写代码是并行(同时发生)。

Q2:单核 CPU 上开多线程有意义吗?为什么?

有意义。因为并发解决的是"等待问题"而不是"计算问题"------单核多线程时,一个线程在等 IO(比如网络、磁盘)的空档,CPU 可以切去执行另一个线程,避免空等浪费。但如果任务是纯 CPU 密集型计算,单核开再多线程也不会变快,还会因为线程切换产生额外开销。

Q3:start()run() 直接调用有什么区别?

start() 会向操作系统申请新线程,真正让代码跑在新的调用栈上,状态变为 RUNNABLE;直接调用 run() 只是把它当成一个普通方法在当前线程里同步执行,根本不会创建新线程,也不会有并发效果。

Q4:sleep()wait() 的区别,为什么生产者-消费者模型不能用 sleep 代替 wait

核心区别是"是否释放锁":sleep 睡眠期间不释放锁,wait 会释放锁并且必须在 synchronized 内调用。生产者-消费者模型里,消费者发现队列为空需要等待,如果用 sleep,它会攥着锁睡觉,生产者根本抢不到锁去放数据,双方永久卡住;用 wait 则会释放锁,让生产者有机会进来把数据放进去,再 notify 唤醒消费者。

Q5:线程调用 sleep() 期间,它持有的锁会被释放吗?

不会。这也是 sleepwait 最本质的区别,也是很多人写出"假死锁"代码的原因------以为 sleep 一下让别的线程先跑,结果因为锁没释放,其他线程照样进不来。


下一篇预告

Day02 会讲讲创建线程的 3 种方式------继承 Thread、实现 Runnable、实现 Callable,它们各自的优缺点,以及为什么大部分场景下更推荐 Runnable/Callable

相关推荐
懒人wsh6 小时前
不上悲观锁也不上 Redis:一个 AI 平台的钱包并发是怎么搞的
后端
奥莱维6 小时前
【无标题】
java·前端·javascript
神奇小汤圆6 小时前
记一次线上翻车:加了Redisson分布式锁,数据还是被并发打穿了
后端
南雨北斗7 小时前
ThinkPHP6 设置响应 `Content-Type: application/json`
后端
用户3945071778247 小时前
Git 安装与 SSH 公钥配置教程
后端
风流 少年7 小时前
Spring AI 2.0:Hello World
java·人工智能·spring
qq_589666057 小时前
Java继承
java·开发语言