并发 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

相关推荐
神奇小汤圆1 小时前
线程池拒绝策略CallerRunsPolicy反而卡死了主线程
后端
神奇小汤圆1 小时前
Jaws:从零构建一个”五脏俱全”的 Java RPC 框架
后端
Csvn2 小时前
📊 SQL 入门 Day 10:递归 CTE — 破解无限层级查询的终极武器
后端·sql
echohelloworld112 小时前
HarmonyOS开发实战:小分享-CreateSelectPage创建分享类型选择器
后端
噢,我明白了2 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
吠品2 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘2 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
用户69371750013842 小时前
从代码生产者到 AI 协作者:软件工程师的角色重构
android·前端·后端
Java内核笔记2 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端