1. 完整回答
Java 线程是程序内部的一条独立执行路径,用来实现多个任务的并发执行。一个 Java 进程可以包含多个线程,这些线程共享进程中的堆内存和对象资源,同时拥有各自独立的程序计数器和线程栈。
线程主要解决的是任务并发处理和程序响应效率的问题。比如 Web 服务处理请求时,可以通过多个线程同时处理不同用户的业务操作;对于多核 CPU,多个线程还可以真正并行执行计算任务。
从执行原理来看,Java 使用 Thread 类表示线程。调用 start() 后,线程进入可运行状态,由调度器安排执行。传统的平台线程通常与操作系统线程一一对应,由操作系统负责调度。执行过程中,线程可能因为等待锁、等待其他线程或者休眠而进入不同状态,最终执行完成后终止。
Java 线程有两个重要特点。第一,线程之间可以共享数据,协作成本较低,但多个线程同时修改共享变量时,可能出现竞态条件,需要通过 synchronized、volatile 或原子类等机制保证线程安全。第二,线程的创建和切换存在开销,因此传统平台线程通常通过线程池复用。
从 Java 21 开始,虚拟线程正式引入,能够以更低的资源开销支持大量并发任务,尤其适合网络请求、数据库访问等 I/O 密集型场景。
实际开发中,Java 线程主要应用于异步任务处理、并发请求处理、后台任务执行以及多核并行计算。
2. 核心原理与关键概念
2.1 线程的执行原理
Java 线程的执行可以概括为:
创建线程 → 启动线程 → 调度执行 → 状态切换 → 线程终止
第一步:创建线程
通过 Thread 创建线程对象,并通过 Runnable 指定执行任务。也可以将任务交给线程池,由线程池管理实际执行的线程。
第二步:启动线程
调用 start() 后,JVM 安排线程启动,线程进入 RUNNABLE 状态,等待或接受调度执行。
平台线程通常由操作系统调度,虚拟线程由 Java 运行时调度到承载它的平台线程上执行。
第三步:执行任务
线程执行 run() 中的任务逻辑。同一进程中的线程共享堆内存,但各自维护独立的调用栈,因此不同线程可以执行不同方法,也可以访问同一个对象。
第四步:状态切换
线程执行过程中可能等待锁、等待其他线程完成任务,或者主动休眠。等待条件满足后,线程重新进入可运行状态。
第五步:线程终止
当 run() 方法正常返回,或者因为未捕获的异常而结束时,线程进入 TERMINATED 状态。
已经启动过的线程无法再次启动。
2.2 Java 线程的六种状态
Java 通过 Thread.State 定义了六种线程状态:
| 线程状态 | 含义 | 典型场景 |
|---|---|---|
NEW |
新建状态,尚未启动 | 创建 Thread 对象 |
RUNNABLE |
可运行状态,包含正在执行和等待 CPU 的情况 | 调用 start() |
BLOCKED |
等待获取对象监视器锁 | 进入被其他线程占用的 synchronized |
WAITING |
无限期等待特定条件 | wait()、join()、park() |
TIMED_WAITING |
在指定时间内等待 | sleep()、带超时的 wait()、join() |
TERMINATED |
线程执行结束 | run() 执行完毕或异常终止 |
常见状态变化:
NEW → RUNNABLE → TERMINATED
执行过程中也可能发生:
RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → RUNNABLE
需要注意,RUNNABLE 同时涵盖就绪和运行状态,Java 没有单独定义 RUNNING 状态。
另外,BLOCKED 专门表示等待对象监视器锁,等待 ReentrantLock 时通常表现为其他等待状态。
2.3 线程安全与内存可见性
多个线程共享对象时,可能出现三类问题:
| 关键概念 | 含义 | 典型处理方式 |
|---|---|---|
| 原子性 | 一个操作整体完成,不会被其他线程交错破坏 | synchronized、原子类 |
| 可见性 | 一个线程修改共享变量后,其他线程能够观察到修改 | volatile、synchronized |
| 有序性 | 保证并发操作满足规定的执行与观察顺序 | Java 内存模型中的同步规则 |
这些行为由 Java 内存模型(JMM) 约束。
其中,happens-before 规则用于建立线程之间的可见性和顺序保证。
例如:
线程 A 修改共享数据 → 释放锁 → 线程 B 获取同一把锁 → 读取共享数据
这种同步关系能够保证线程 B 看到线程 A 在释放锁之前完成的修改。
3. 高频面试追问
3.1 Java 有哪些创建和使用线程的方式?
Java 常见的线程使用方式包括继承 Thread、实现 Runnable、使用 Callable 配合 FutureTask,以及通过线程池提交任务。
继承 Thread 可以直接重写 run() 方法,但这种方式将任务逻辑与线程对象绑定在一起。
Runnable 用来描述没有返回值的任务,通常配合 Thread 或线程池使用。
Callable 可以返回执行结果,也可以抛出受检异常,适合需要获取异步计算结果的场景,通常结合 Future 使用。
实际项目中,更常见的方式是使用 ExecutorService 提交任务,统一管理线程资源、任务执行和关闭流程。
Java 21 之后,还可以通过 Thread.ofVirtual() 或虚拟线程执行器创建虚拟线程,处理大量并发任务。
继续追问:start() 和 run() 有什么区别?
start() 用于启动新线程,JVM 会安排这个线程执行 run() 方法。
直接调用 run() 只是一次普通方法调用,不会启动新线程。对于普通平台线程,如果直接调用执行了任务,任务仍运行在调用者线程中。
另外,同一个线程对象只能调用一次 start(),重复启动会抛出 IllegalThreadStateException。
3.2 Java 线程为什么有六种状态?BLOCKED 和 WAITING 有什么区别?
Java 将线程划分为六种状态,主要用于描述线程从创建、执行、等待到终止的生命周期。
其中,最容易混淆的是 BLOCKED 和 WAITING。
BLOCKED 表示等待进入 synchronized 保护的临界区。 比如线程 A 持有某个对象锁,线程 B 也需要获取这把锁,线程 B 就会进入阻塞状态。
WAITING 表示线程正在等待某个条件或事件。 比如调用 wait() 等待通知,或者调用 join() 等待另一个线程结束。
两者的关键区别是等待原因不同:一个在等待对象监视器锁,一个在等待特定事件或条件。
如果使用 sleep() 或带超时的等待方法,线程通常会进入 TIMED_WAITING 状态。
3.3 sleep()、wait() 和 join() 有什么区别?
三个方法都可能让当前线程暂停执行,但作用不同。
Thread.sleep() 让当前线程休眠指定时间,不会释放已经持有的锁。通常用于定时等待、模拟耗时操作等场景。
Object.wait() 用于线程之间的条件等待。调用前必须持有对应对象的监视器锁,调用后会释放该对象的锁,等到被通知、被中断或发生其他合法唤醒情况后,再重新竞争锁。
Thread.join() 用来等待指定线程执行结束。例如主线程启动一个工作线程后,调用它的 join(),主线程就会等待该工作线程结束后再继续执行。
所以,sleep() 主要控制休眠时间,wait() 用于线程协作,join() 用于等待线程结束。
继续追问:为什么 wait() 必须在 synchronized 中调用?
因为 wait() 基于对象监视器实现线程协作,调用线程必须先持有对应对象的锁,才能安全地释放锁并进入等待集合。
如果没有持有对应的监视器锁,调用 wait() 会抛出 IllegalMonitorStateException。
3.4 多个线程同时修改一个变量,为什么会出现线程安全问题?
核心原因是多个线程对共享变量的操作可能发生交错执行,而且线程之间不一定能够及时观察到对方的修改。
例如,两个线程同时执行 count++,这个操作实际涉及读取、加一和写回三个环节。
如果两个线程同时读取到相同的旧值,最后可能都写入相同的新值,导致一次更新丢失。
解决方式需要根据业务选择:
synchronized 可以通过互斥锁保护整个更新过程,同时提供可见性保证。
AtomicInteger 可以通过原子操作实现线程安全的计数,通常使用 CAS 等底层机制。
volatile 适合状态标志等场景,能够提供可见性和一定的有序性保证,但无法让 count++ 这样的复合操作具备原子性。
因此,简单计数可以使用原子类,涉及多个共享变量的一致性操作通常需要加锁。
继续追问:CAS 为什么能够保证原子性?
CAS 是比较并交换操作,会比较当前值与预期值,只有相等时才执行更新,整个比较和更新过程具有原子性。
如果其他线程已经修改了数据,CAS 就会失败,调用方可以重新读取并重试。
它适合单个变量的并发更新,但高竞争下可能产生较多重试,而且复杂的多变量操作仍需要其他同步方案。
3.5 interrupt() 能直接停止线程吗?
interrupt() 用于向线程发送中断请求,通常不会直接强制终止线程。
每个线程都有中断状态。调用 interrupt() 后,目标线程可以通过检查中断状态决定是否退出。
如果线程正在执行 sleep()、wait() 或 join() 等可中断等待操作,通常会抛出 InterruptedException,并清除中断标志。
因此,处理线程取消时,通常会让任务主动检查中断状态,收到中断后清理资源并正常退出。
如果捕获了 InterruptedException,但当前方法无法继续向上抛出,也不打算立即结束任务,通常需要通过 Thread.currentThread().interrupt() 恢复中断标志,避免丢失取消信号。
实际项目中,线程池任务可以结合 Future.cancel(true) 请求中断正在执行的任务,但最终能否及时退出,仍然取决于任务代码和阻塞操作是否支持中断。
3.6 多个线程之间如何进行通信和协作?
Java 线程之间可以通过共享变量、锁和并发工具类进行协作。
底层方式是 wait()、notify() 和 notifyAll()。
例如生产者消费者模型:
消费者发现队列为空 → 调用 wait() 释放锁并等待 → 生产者放入数据并通知 → 消费者重新获取锁并检查条件 → 消费数据
其中,notify() 只负责通知等待线程,被唤醒的线程还需要重新获得对象锁才能继续执行。
另外,使用 wait() 时通常要通过 while 循环检查等待条件,因为线程可能出现虚假唤醒,也可能在重新获取锁之前被其他线程改变条件。
实际开发中,更常使用 BlockingQueue 实现生产者消费者模型,使用 CountDownLatch 等待多个任务完成,或者使用 Semaphore 控制并发访问数量。
这些工具能够减少手动处理线程等待和通知的复杂度。
3.7 什么是线程死锁?如何避免和排查?
死锁是指多个线程相互等待对方持有的资源,导致所有相关线程都无法继续执行。
例如,线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1,双方就形成了循环等待。
死锁通常涉及四个条件:互斥、持有并等待、不可抢占以及循环等待。
实际开发中,最常见的预防方式是统一锁的获取顺序。
例如所有线程都先获取锁 1,再获取锁 2,就可以避免这两把锁之间形成反向循环等待。
还可以缩小锁范围,减少嵌套加锁,或者使用 ReentrantLock.tryLock() 配合超时机制,降低无限等待的风险。
排查死锁时,可以通过 jstack、jcmd 等工具查看线程转储,分析线程持有的锁和正在等待的锁,定位循环依赖关系。
3.8 为什么实际项目更倾向于使用线程池?线程池如何处理任务?
线程池的核心作用是复用平台线程、控制资源消耗和统一管理任务。
对于传统平台线程,频繁创建和销毁线程会产生资源开销。线程池通过维护一定数量的工作线程,让多个任务重复使用这些线程。
以 ThreadPoolExecutor 为例,任务的处理过程通常是:
提交任务 → 核心线程不足则创建线程 → 核心线程已达到配置数量则尝试入队 → 队列无法接收任务时尝试扩容 → 达到最大线程数仍无法处理则执行拒绝策略。
配置线程池时,需要关注核心线程数、最大线程数、任务队列容量、空闲线程存活时间以及拒绝策略。
例如订单系统需要异步处理通知和日志,可以为相关任务配置独立的有界线程池,避免某类任务大量积压后影响其他业务。
线程数量通常结合任务类型、CPU 资源、阻塞时间和实际压测结果确定。CPU 密集型任务主要受计算核心数限制,I/O 密集型任务通常可以容纳更多并发等待。
继续追问:为什么不直接使用无限队列?
无限队列会让任务在处理速度跟不上提交速度时持续积压,增加内存占用和请求等待时间,严重时可能导致内存耗尽。
而且使用无界队列时,ThreadPoolExecutor 通常不会继续扩容超过核心线程数。
因此,生产环境中通常会结合业务容量设置有界队列,并配置合理的拒绝策略和监控指标。
3.9 ThreadLocal 有什么作用?在线程池中有什么问题?
ThreadLocal 用来为每个线程维护独立的变量值,适合存储线程上下文信息。
例如,在请求处理过程中,可以通过 ThreadLocal 保存请求标识、日志追踪信息等,使同一线程中的不同方法能够访问相应上下文。
它的核心机制是每个线程维护自己的线程局部变量数据,不同线程访问同一个 ThreadLocal 时,获取的是各自关联的值。
需要注意,ThreadLocal 只能实现线程之间的数据隔离,无法自动保证共享对象内部的线程安全。
在线程池中,由于工作线程会被反复使用,如果任务结束后没有清理 ThreadLocal,后续任务可能读取到上一个任务遗留的数据,同时还可能导致对象被长期引用,占用内存。
因此,在线程池任务中使用 ThreadLocal,通常需要通过 try-finally 在任务结束时调用 remove()。
3.10 Java 虚拟线程和传统平台线程有什么区别?适合什么场景?
Java 21 正式引入了虚拟线程,其主要目标是降低大量并发任务带来的线程资源开销。
传统平台线程通常与操作系统线程一一对应,创建和运行需要占用操作系统线程资源,因此数量受到较明显的资源限制。
虚拟线程由 JVM 调度,多个虚拟线程可以复用较少的平台线程。
当虚拟线程执行受支持的阻塞 I/O 操作时,JVM 可以将虚拟线程从承载它的平台线程上卸载,使平台线程继续执行其他虚拟线程。等 I/O 完成后,再调度虚拟线程恢复执行。
因此,虚拟线程非常适合大量并发请求、数据库访问和远程服务调用等 I/O 密集型任务。
不过,虚拟线程主要改善并发扩展能力,不会直接提高 CPU 的计算速度,也无法消除数据库连接数、接口限流等外部资源限制。
另外,虚拟线程通常按任务创建,不需要像传统平台线程一样建立线程池复用。需要限制外部资源访问数量时,可以使用 Semaphore。
继续追问:虚拟线程能够完全避免阻塞平台线程吗?
不能。对于受支持的阻塞操作,虚拟线程通常可以卸载并释放承载线程,但某些本地方法和外部函数调用仍可能造成线程固定,使承载线程无法被其他虚拟线程复用。
因此,虚拟线程适合大量等待型任务,实际吞吐量仍然需要结合阻塞类型、外部资源容量和运行环境进行评估。
4. 易混淆概念对比
4.1 进程与线程
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 基本含义 | 程序运行的资源隔离单位 | 进程内的执行路径 |
| 内存关系 | 通常具有独立地址空间 | 共享进程内存,各自拥有独立栈 |
| 资源开销 | 通常较高 | 通常较低 |
| 通信方式 | 进程间通信机制 | 共享内存、同步工具 |
| 主要问题 | 进程间通信和资源管理 | 竞态条件、线程同步、死锁 |
4.2 Runnable 与 Callable
| 对比维度 | Runnable |
Callable |
|---|---|---|
| 执行方法 | run() |
call() |
| 返回值 | 无 | 有 |
| 受检异常 | 不能直接向外声明抛出 | 可以声明抛出 |
| 典型场景 | 普通异步任务 | 需要返回结果的异步任务 |
| 常见用法 | Thread、线程池 |
线程池、FutureTask |
4.3 start() 与 run()
| 对比维度 | start() |
run() |
|---|---|---|
| 主要作用 | 启动线程 | 描述或执行任务逻辑 |
| 是否启动新线程 | 是 | 否 |
| 执行线程 | 新启动的线程 | 直接调用时使用当前线程 |
| 重复调用 | 同一线程对象不能重复启动 | 作为普通方法可重复调用 |
4.4 sleep()、wait() 与 join()
| 对比维度 | sleep() |
wait() |
join() |
|---|---|---|---|
| 所属类 | Thread |
Object |
Thread |
| 作用 | 线程休眠 | 等待条件或通知 | 等待目标线程结束 |
| 是否释放已持有的监视器锁 | 否 | 释放调用对象对应的锁 | 不会主动释放调用者已持有的锁 |
| 是否需要持有指定锁 | 否 | 是 | 否 |
| 典型用途 | 延迟执行 | 线程协作 | 执行顺序控制 |
4.5 volatile、synchronized 与 AtomicInteger
| 对比维度 | volatile |
synchronized |
AtomicInteger |
|---|---|---|---|
| 核心机制 | 内存可见性与有序性保证 | 对象监视器锁 | 原子操作,常用 CAS |
| 保证可见性 | 是 | 是 | 是 |
| 保证复合操作原子性 | 否 | 受锁保护的操作可以 | 支持其提供的原子操作 |
| 是否使用互斥锁 | 否 | 是 | 通常不使用互斥锁 |
| 典型场景 | 状态标志 | 多步骤共享状态更新 | 并发计数 |
4.6 平台线程与虚拟线程
| 对比维度 | 平台线程 | 虚拟线程 |
|---|---|---|
| 调度方式 | 操作系统调度 | JVM 调度 |
| 线程映射 | 通常与操作系统线程一一对应 | 多个虚拟线程复用平台线程 |
| 创建开销 | 相对较高 | 相对较低 |
| 适合场景 | 通用任务、CPU 密集型任务 | 大量 I/O 等待型任务 |
| 管理方式 | 常使用线程池复用 | 通常每个任务创建一个虚拟线程 |
| 主要限制 | 线程资源和切换开销 | CPU 资源、外部容量及部分阻塞操作 |
5. 总结
Java 线程是进程内部的独立执行路径,主要用于实现任务并发和多核并行计算。不同线程共享进程中的对象资源,同时拥有独立的执行栈。线程通过 start() 启动,由调度器安排执行,运行过程中可能因为锁竞争、条件等待或休眠发生状态切换。由于共享数据可能出现竞态条件,需要使用锁、原子类或其他并发工具保证线程安全。实际开发中,传统平台线程通常通过线程池复用,以控制资源消耗;Java 21 引入的虚拟线程则通过轻量级调度提高大量 I/O 等待任务的并发处理能力。线程能够提高资源利用率,但过多的线程、锁竞争和外部资源限制仍然可能影响系统性能。