大家好,我是晚安code。
JUC并发编程这门课几乎是 Java 后端面试的必考点——从「线程有几种状态」到「手写生产者消费者」,一问一个准。这篇就把我踩过的坑和整理好的思路一次性给你:线程状态、Lock 与 Synchronized 的区别、还有那个让无数人翻车的虚假唤醒。收藏一下,面试前翻出来救急。
一、JUC 是什么:并发编程的「工具包」
JUC 不是一门新语言,它是 Java 官方写给并发编程的标准答案,藏在 java.util.concurrent 这个包里。
JUC(java.util.concurrent):Java 官方提供的并发编程工具包,把锁、线程池、并发集合都收在了这一个包下。你可以理解为「Java 并发编程的百宝箱」。
自己写业务时,用普通 Thread 开线程效率并不高。传统 Runnable 接口没有返回值,回调结果全靠共享变量,又麻烦又容易出并发问题;Callable 接口能返回结果,配合 Future 才能拿到线程的返回值。JUC 就是把这一套高效写法给做成了标准库。
二、线程与进程:并发和并行别搞混
并发和并行是 JUC 学习路上第一个绊脚石,分不清它,后面全是糊涂账。
先说进程和线程:进程是一个程序的集合,比如 QQ.exe 就是一个进程,一个进程至少包含一个线程。线程是进程里的执行单元,比如你用 Typora 打字的同时它还在自动保存——写字和保存就是两个线程在干活。Java 程序默认开两个线程:main 主线程和 GC 垃圾回收线程。
有个冷知识:Java 本身是开不了线程的。Thread.start() 底层调用的是 native start0(),这是本地方法,由底层的 C++ 操作操作系统去创建线程,Java 只负责把请求递下去。
并发:多个线程在同一段时间内交替执行同一个资源,单核 CPU 也能模拟出来。可以理解为「一条路,多辆车轮流过」。
并行:多个线程在多个 CPU 核上同时执行,真正的同时干活。可以理解为「多条路,车同时开」。
并发编程的本质就一句话:充分利用 CPU 资源。通过 Runtime.getRuntime().availableProcessors() 可以拿到 CPU 核数,写线程池时区分 CPU 密集型和 IO 密集型的核心线程数就用它。

三、线程的六种状态 + wait/sleep 区别
线程状态的管理,本质是 JVM 搭好的一套流转规则,记住六种状态,面试第一问基本就稳了。
Java 线程一共有六种状态,定义在 Thread.State 枚举里:
| 状态 | 含义 | 进入方式 |
|---|---|---|
| NEW | 新生,线程刚创建没 start | new Thread() |
| RUNNABLE | 运行中,等待 CPU 调度 | start() |
| BLOCKED | 阻塞,竞争锁失败排队 | 进入同步代码块 |
| WAITING | 等待,一直等被唤醒 | wait()、join() |
| TIMED_WAITING | 超时等待,到点自己醒 | sleep()、wait(毫秒) |
| TERMINATED | 终止,run() 执行完 | 正常结束 |
我用一张图把它串起来,面试时照着这个思路说就行:

wait 和 sleep 是面试最爱问的一对兄弟,因为它俩长得像,本质上完全不一样。

我整理成一张表
| 对比项 | wait | sleep |
|---|---|---|
| 来自哪个类 | Object | Thread |
| 是否释放锁 | 释放 | 不释放 |
| 使用范围 | 必须在同步代码块中 | 任何地方都能用 |
| 是否必须捕获异常 | 不需要 | 必须捕获 |
企业里写休眠推荐用 TimeUnit 工具类,比裸写 Thread.sleep 语义清晰得多:
TimeUnit.DAYS.sleep(1); // 睡 1 天
TimeUnit.SECONDS.sleep(2); // 睡 2 秒
四、Lock 锁:从 synchronized 到 ReentrantLock
Lock 是 JUC 里最该先掌握的锁,它把 synchronized 的短板全补上了。先看传统写法。
传统 synchronized 解决并发问题,本质就两个字:队列 + 锁。多个线程同时卖票,不加锁就会超卖:
class Ticket {
private int number = 50;
public synchronized void sale() {
// synchronized 本质:排队 + 加锁
if (number > 0) {
System.out.println(Thread.currentThread().getName()
+ " 卖出了 " + (number--) + " 票,剩余 " + number);
}
}
}
synchronized 用起来省心,但它有几个短板:拿不到锁状态、线程只能一直等、锁非公平、不可中断。JUC 的 Lock 接口就是来解决这些问题的。
Lock:JUC 提供的锁接口,最常用的实现是 ReentrantLock。你可以把它想成一把「手动挡」的锁,加锁解锁都得自己来。
class Ticket2 {
private int number = 50;
Lock lock = new ReentrantLock(); // 1. 建锁
public void sale() {
lock.lock(); // 2. 加锁
try {
if (number > 0) {
/* 卖票业务 */ }
} finally {
lock.unlock(); // 3. 解锁,不写就是死锁
}
}
}
lock 三部曲:① new ReentrantLock() 建锁 → ② lock.lock() 加锁 → ③ finally 里 lock.unlock() 解锁。第三步最关键,忘了写整个程序直接卡死。
ReentrantLock 还分公平锁和非公平锁,默认是非公平:
公平锁:先来后到,谁先排队谁先拿到锁。可以理解为食堂打饭排好队,不乱。
非公平锁:允许插队,刚释放的锁可能被后来的线程抢走(默认)。可以理解为排队时突然有人插队。
构造时传 true 就是公平锁:new ReentrantLock(true);不传参数默认非公平:new ReentrantLock()。
synchronized 和 Lock 怎么选,一张表说清楚:
| 对比项 | synchronized | Lock |
|---|---|---|
| 判断锁状态 | 做不到 | tryLock() 能判断 |
| 释放锁 | 自动释放 | 必须手动,finally 里 unlock |
| 等待方式 | 线程一直等 | 不会一直等,可超时 |
| 公平性 | 只能非公平 | 可设置公平/非公平 |
| 适用场景 | 少量同步代码 | 大量同步代码 |

五、生产者消费者问题:从 wait/notify 到 Condition
生产者消费者问题是 JUC 面试的必考题,和单例模式、排序算法、死锁并称「面试四件套」。考的不是你会不会写,而是你懂不懂等待-通知这套机制。
经典场景:一个数据类,increment 加 1、decrement 减 1,生产线程 A 加了要通知消费者 B 来减。synchronized 版本的套路是四步:判断 → 等待 → 业务 → 通知。
public synchronized void increment() throws InterruptedException {
while (number != 0) {
// 1. 判断:number 不是 0 就等
this.wait(); // 2. 等待
}
number++; // 3. 业务:加 1
this.notifyAll(); // 4. 通知:唤醒其他线程
}
我第一次学习时写这段代码时,只有 A、B 两个线程跑得好好的,结果突发奇想我换成 A/B/C/D 四个线程,当场就翻车了——数据直接变成负数。

原因就是我用了 if 判断,这就是传说中的虚假唤醒。
可能有人会问:什么情况会被虚假唤醒?
当有多个线程同时 wait 在同一个条件上时,某个线程可能没收到通知、没被中断、也没超时,却被莫名唤醒。Java 官方文档明确建议:wait 必须放在循环里(
while判断),等条件真正满足再继续。把if改成while就彻底解决——每次醒来重新判断一次,不满足就继续等。
虚假唤醒(Spurious Wakeup):线程没收到明确通知却被意外唤醒的现象。可以理解为「闹钟还没响,你却提前醒了」。


而 JUC 版的生产者消费者,把 wait/notify 换成了 Lock + Condition,套路一模一样,只是换了 API:
Condition:和 Lock 配套的等待/通知工具,把 wait/notify 从 Object 搬到了锁上。可以理解为锁的「对讲机」,用它喊话唤醒线程。
class Data {
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await(); // 等待(替代 wait)
}
number++;
condition.signalAll(); // 通知(替代 notifyAll)
} finally {
lock.unlock();
}
}
}
整个流程还是「判断 → 等待 → 业务 → 通知」四步,一张图看清:

JUC 版的好处是:Condition 可以创建多个,实现精准唤醒——比如唤醒「加 1 的」和唤醒「减 1 的」分开,比 notifyAll 一把梭精准得多,这也是 JUC 里 ArrayBlockingQueue 等并发容器底层正在用的思路。
我把整个 JUC并发编程学习笔记按这条主线整理完了:并发/并行 → 线程状态 → wait/sleep → Lock → 生产者消费者。这篇算 JUC 的地基,
我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你学 JUC 时踩过哪些并发坑?wait 和 sleep 第一次分清楚了吗?