Java 后端面试里,并发几乎是必问模块。
很多同学一开始学并发时,会觉得概念很多:进程、线程、线程状态、线程安全、synchronized、volatile、原子性、可见性、有序性……
这些词单独看都不难,但如果没有串起来,很容易背得零散。
这篇文章就从最基础的进程和线程开始,逐步讲到 Java 线程状态、线程安全问题,以及 synchronized 和 volatile 的区别。
一、什么是进程?什么是线程?
进程是操作系统分配资源的基本单位。
线程是 CPU 调度执行的基本单位。
可以简单理解为:
进程:一个正在运行的程序 线程:进程里面的一条执行路径
比如你启动一个 Java 程序,这个 Java 程序就是一个进程。
这个程序里面可以有多个线程同时工作,比如主线程、GC 线程、业务线程、线程池里的工作线程。
二、进程和线程有什么区别?
| 对比 | 进程 | 线程 |
|---|---|---|
| 资源 | 拥有独立内存空间 | 共享进程内存 |
| 开销 | 创建和切换开销大 | 创建和切换开销较小 |
| 通信 | 进程间通信较复杂 | 线程间共享变量更方便 |
| 稳定性 | 一个进程崩溃一般不影响其他进程 | 一个线程异常可能影响整个进程 |
面试可以这样回答:
进程是资源分配单位,线程是 CPU 调度单位。同一进程内多个线程共享内存,所以通信方便,但也会带来线程安全问题。
三、为什么要使用多线程?
使用多线程主要有几个原因:
提高 CPU 利用率 提高程序响应速度 处理并发请求 异步执行耗时任务
比如 Java Web 服务中,请求通常由线程池中的线程处理。
如果只有一个线程,多个请求只能排队执行,吞吐量会很低。
多线程可以让多个任务并发执行,提高系统整体处理能力。
四、线程越多越好吗?
不是。
线程太多会带来很多问题:
线程切换开销大 占用内存 锁竞争加剧 系统负载过高
每个线程都需要栈内存,线程越多,占用内存越多。
另外,CPU 核心数是有限的。线程太多时,操作系统会频繁在线程之间切换,反而降低性能。
所以实际项目中不会无限创建线程,而是使用线程池管理线程。
五、Java 创建线程有哪几种方式?
常见说法有四种:
继承 Thread 实现 Runnable 实现 Callable + FutureTask 使用线程池
最简单的写法:
new Thread(() -> { System.out.println("hello"); }).start();不过实际开发中更推荐使用线程池,而不是频繁手动 new Thread()。
因为线程创建和销毁都有成本,线程池可以复用线程,也能统一控制并发数量。
六、Runnable 和 Callable 有什么区别?
| 对比 | Runnable | Callable |
|---|---|---|
| 返回值 | 没有 | 有 |
| 异常 | 不能直接抛 checked exception | 可以抛出 |
| 方法 | run() | call() |
示例:
Callable<Integer> task = () -> 1 + 1;Callable 通常配合 Future 或线程池使用,可以拿到异步任务的执行结果。
七、调用 start() 和 run() 有什么区别?
start() 是启动一个新线程。
thread.start();JVM 会创建新线程,并在线程中执行 run() 方法。
如果直接调用:
thread.run();那只是普通方法调用,不会创建新线程。
面试重点:
启动线程必须调用 start(),直接调用 run() 不会开启新线程。
八、Java 线程有哪些状态?
Java 线程状态有 6 种:
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
可以用下面这张图理解:
注意:
Java 里的 RUNNABLE 包括操作系统层面的“就绪”和“运行中”。
九、BLOCKED、WAITING、TIMED_WAITING 有什么区别?
| 状态 | 含义 | 常见场景 |
|---|---|---|
| BLOCKED | 等待 synchronized 锁 | 进入同步代码块但锁被别人占用 |
| WAITING | 无限期等待,需要别人唤醒 | wait()、join()、LockSupport.park() |
| TIMED_WAITING | 带时间的等待 | sleep(1000)、wait(1000)、join(1000) |
简单记:
BLOCKED:等锁 WAITING:一直等别人叫醒 TIMED_WAITING:等一段时间
十、sleep() 和 wait() 有什么区别?
这是 Java 并发超高频题。
| 对比 | sleep | wait |
|---|---|---|
| 所属 | Thread 类 | Object 类 |
| 是否释放锁 | 不释放锁 | 释放锁 |
| 使用位置 | 任意地方 | 必须在 synchronized 中 |
| 唤醒方式 | 时间到了自动醒 | notify/notifyAll 或超时 |
重点记:
sleep 不释放锁 wait 会释放锁
示例理解:
线程拿着锁 sleep,别人还是进不来; 线程执行 wait,会释放锁,让别人有机会进入同步代码块。
十一、什么是线程安全?
线程安全指的是:
多个线程同时访问同一份共享数据时,程序结果仍然正确。
比如多个线程同时执行:
count++;
如果没有同步控制,最后结果可能小于预期。
因为 count++ 不是一个原子操作。
十二、为什么 count++ 不是线程安全的?
count++ 看起来是一行代码,但底层大概有三步:
1. 读取 count 2. count + 1 3. 写回 count
两个线程可能同时读到 count = 0:
线程 A 读到 0,加 1,写回 1 线程 B 读到 0,加 1,写回 1
执行了两次自增,结果却是 1。
这就是并发下的数据丢失。
流程图:
十三、并发问题的根源有哪些?
常见有三个:
原子性 可见性 有序性
| 问题 | 含义 |
|---|---|
| 原子性 | 一个操作不可被打断 |
| 可见性 | 一个线程修改变量,其他线程能及时看到 |
| 有序性 | 程序执行顺序不会因为重排序导致错误 |
synchronized 可以保证原子性、可见性、有序性。
volatile 主要保证可见性和一定的有序性,但不保证复合操作的原子性。
十四、synchronized 是什么?
synchronized 是 Java 内置锁,也叫监视器锁。
它可以修饰:
普通方法 静态方法 代码块
示例:
synchronized (this) { count++; }作用是:
同一时刻只允许一个线程进入同步代码块
这样可以保证共享变量修改的线程安全。
十五、synchronized 锁的是什么?
synchronized 锁的是对象。
不同写法锁对象不同。
普通同步方法:
public synchronized void method() { }锁的是当前实例对象:
this
静态同步方法:
public static synchronized void method() { }锁的是当前类的 Class 对象。
同步代码块:
synchronized (lock) { }锁的是括号里的 lock 对象。
面试时一定要说清楚:
synchronized 不是锁代码,而是锁对象。
十六、synchronized 能保证什么?
synchronized 主要保证:
原子性 可见性 有序性
进入 synchronized 前需要获得锁。
退出 synchronized 时,会把工作内存中的共享变量刷新到主内存。
其他线程再进入 synchronized 时,会从主内存读取最新值。
所以它既能防止并发修改,也能让其他线程看到最新结果。
十七、volatile 是什么?
volatile 是 Java 关键字,用来修饰变量。
它主要保证:
可见性 禁止指令重排序
示例:
private volatile boolean running = true;一个线程修改:
running = false;另一个线程能尽快看到变化。
十八、volatile 能保证原子性吗?
不能。
比如:
volatile int count = 0; count++;仍然不是线程安全的。
因为 count++ 是读、改、写三个步骤。
volatile 只能保证每次读到的是较新的值,但不能保证这三个步骤不可被打断。
如果要保证自增的原子性,可以用:
AtomicInteger
或者:
synchronized
十九、volatile 常见使用场景是什么?
常见场景:
状态标记 双重检查锁定 DCL 停止线程标志
比如停止线程:
private volatile boolean stop = false; while (!stop) { // do something }另一个线程设置:
stop = true;工作线程能及时看到变化并退出。
二十、synchronized 和 volatile 有什么区别?
| 对比 | synchronized | volatile |
|---|---|---|
| 原子性 | 保证 | 不保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证 | 一定程度保证 |
| 是否阻塞 | 可能阻塞 | 不阻塞 |
| 使用场景 | 复合操作、临界区 | 状态标记、配置开关 |
面试回答:
synchronized 更重,能保证原子性、可见性和有序性,适合保护临界区; volatile 更轻量,只保证可见性和禁止重排序,适合一个线程写、多个线程读的状态标记,不适合 count++ 这种复合操作。
二十一、这一组怎么串起来讲?
可以这样回答:
进程是资源分配单位,线程是 CPU 调度单位。
同一进程内线程共享内存,所以通信方便,但也会带来线程安全问题。
Java 创建线程可以继承 Thread、实现 Runnable、实现 Callable,实际开发更推荐线程池。
启动线程要调用 start,直接调用 run 只是普通方法调用。
Java 线程有 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 六种状态。
线程安全问题主要来自原子性、可见性、有序性。 synchronized 锁的是对象,能保证原子性、可见性、有序性; volatile 保证可见性和禁止重排序,但不保证 count++ 这类复合操作的原子性。
总结
这一组可以按下面这条线来记:
进程是资源分配单位,线程是 CPU 调度单位。
多线程能提高并发处理能力,但线程不是越多越好。
启动线程要调用 start,不是 run。
Java 线程有 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 六种状态。
BLOCKED 是等 synchronized 锁,WAITING 是无限等待,TIMED_WAITING 是限时等待。
sleep 不释放锁,wait 会释放锁。
线程安全问题来自原子性、可见性、有序性。 synchronized 锁的是对象,能保证原子性、可见性、有序性。 volatile 保证可见性和禁止重排序,但不保证 count++ 的原子性。
这一组重点背:进程 vs 线程、start vs run、线程状态、sleep vs wait、线程安全三大问题、synchronized 锁对象、volatile 可见性、volatile 不保证原子性。
📌 码字不易,技术干货深度复盘!
如果这篇文章帮你看清了 MyBatis-Plus 查询的底层底细,别忘了 点赞、关注、收藏 三连走一波!支持作者不迷路,更多底层源码干货持续输出中!🚀
让我们一起学习面试知识,拿到自己想要的offer!