作为一名程序员,多线程是并发编程的基础,也是面试和工作中的高频考点。这篇文章整理自我的学习笔记,核心内容参考了《并发编程实战》《计算机原理》等文献,可能与部分书籍内容重合。它并非零基础入门教程,更适合用于梳理知识体系、复习核心要点,若能帮到同样在巩固多线程知识的你,我会十分荣幸。
一、线程与进程:分清这对 "搭档"
理解多线程,首先要搞懂线程与进程的关系 ------ 进程是线程的 "容器",二者既有联系又有明确区别,从四个核心维度就能清晰区分:
对比维度 | 进程 | 线程 |
---|---|---|
粒度 | 操作系统资源分配的基本单位 | CPU调度和执行的基本单位 |
内存 | 占有独立内存空间,进程间内存隔离 | 共享所属进程的内存空间 |
稳定性 | 单个进程异常不影响其他进程 | 单个线程崩溃可能导致整个进程异常 |
消耗 | 创建 / 销毁需分配回收资源、处理页调度,开销大 | 仅需保存寄存器和栈信息,开销小 |
为什么进程开销比线程大?
关键在于 "资源管理" 的差异:
- 创建 / 终止效率:进程需初始化内存管理、文件管理等资源信息,线程直接共享这些信息,因此线程创建 / 终止更快;
- 切换效率:同一进程的线程共享虚拟内存和页表,切换时无需更换页表;而进程切换需重新加载页表,开销显著更高;
- 数据交互效率:线程间共享进程内存,数据传递无需经过内核;进程间通信则需依赖内核机制(如管道、消息队列),效率更低。
二、线程的 "一生":从新建到终止
线程的生命周期包含 7 个状态,每个状态的切换都有明确触发条件,搞懂这些就能掌握线程的运行逻辑:
- New(新建) :在代码中创建线程对象(如
new Thread()
),JVM 为其分配内存、初始化成员变量,但此时还未分配 CPU 执行权,不能运行; - Runnable(就绪) :调用
start()
方法后,JVM 为线程创建方法调用栈和程序计数器,线程进入 "等待 CPU" 的就绪状态;此外,sleep()
结束、join()
结束、线程拿到对象锁等场景,也会让线程进入就绪状态; - Running(运行) :就绪状态的线程获得 CPU 时间片,开始执行
run()
方法中的代码,此时处于运行状态; - Blocked(阻塞) :运行中的线程因 "获取同步锁失败" 进入阻塞状态(比如进入
synchronized
代码块时,锁已被其他线程占用),会被放入锁池的_EntryList
队列等待; - Waiting(等待) :线程执行
wait()
、join()
、park()
等方法后,会进入_WaitSet
队列,需等待其他线程调用notify()
/notifyAll()
唤醒; - Waiting_Timeout(超时等待) :调用带超时时间的方法(如
wait(1000)
、sleep(1000)
),线程会在等待超时后自动唤醒,无需依赖其他线程通知; - Terminated(终止) :线程正常执行完
run()
方法,或因异常中断,生命周期彻底结束。
创建线程的 4 种方法,该怎么选?
实际开发中,创建线程主要有 4 种方式,各有适用场景:
- 继承 Thread 类 :重写
run()
方法,调用start()
启动。优点是在run()
中可直接用this
获取当前线程;缺点是 Java 不支持多继承,扩展性受限,且每次新建任务需创建独立线程,损耗较大。 - 实现 Runnable 接口 :实现
run()
方法,将实例传给Thread
对象。优点是任务与线程解耦,同一任务可传给多个Thread
,适合多线程执行同一任务的场景。 - 实现 Callable 接口 :实现
call()
方法,支持返回执行结果(需配合FutureTask
),且能抛出异常,适合需要获取线程执行结果的场景。 - 使用线程池 / 定时器 :如
Executors
、ThreadPoolExecutor
,能复用线程、减少创建 / 销毁开销,是生产环境的首选方式。
三、线程中断:别用 "暴力停止"
停止线程是个技术活,直接用stop()
"杀死" 线程的方式已被弃用,正确的做法是通过 "通知" 让线程自行终止。
3 种停止线程的方式
- 使用 volatile 退出标志 :定义
volatile boolean isStop = false
,线程在run()
中循环判断isStop
,外部线程修改isStop = true
时,线程正常退出。volatile
保证标志的可见性,避免线程读取到旧值。 - 弃用的 stop () 方法 :调用
stop()
会强制终止线程,且不会执行finally
中的清理操作(如关闭文件、数据库连接),还可能导致线程持有的锁无法释放,引发死锁,绝对不能用。 - 推荐的 interrupt () 方法:这不是 "强制停止",而是给线程打一个 "中断标志",让线程自行判断是否退出,更安全灵活。
interrupt () 与 stop () 的核心区别
特性 | stop() | interrupt() |
---|---|---|
作用方式 | 强制杀死线程 | 仅设置中断标志,通知线程 |
锁释放 | 不释放持有的锁 | 不直接释放锁,线程可自行处理 |
安全性 | 易导致资源泄漏、死锁 | 安全,由线程自主控制退出时机 |
正确使用 interrupt () 的 3 种场景
- 线程处于
sleep()
/wait()
/join()
状态:调用interrupt()
会让线程立即退出阻塞,抛出InterruptedException
,捕获异常后可执行退出逻辑; - 线程处于 I/O 阻塞(如 Socket 读取):会抛出
ClosedByInterruptException
,同样通过捕获异常处理中断; - 线程正常运行:需通过
Thread.interrupted()
或isInterrupted()
判断中断标志,若标志为true
,则主动退出循环。
3 个中断相关方法的区别
interrupt()
:实例方法,给线程设置中断标志,不直接停止线程;interrupted()
:静态方法,判断 "当前线程" 是否被中断,会清除中断标志;isInterrupted()
:实例方法,判断 "调用该方法的线程" 是否被中断,不清除中断标志。
二者底层调用同一方法,仅参数不同:interrupted()
传入true
(清除标志),isInterrupted()
传入false
(不清除)。
中断的典型使用场景
- 某个操作超时(如网络请求超过 5 秒需中止);
- 一组线程中一个出错,需取消其他所有线程;
- 多线程执行同一任务,只要一个线程成功,其他线程可取消。
四、多线程高频问题:梳理核心概念
最后,整合几个多线程的基础高频知识点,帮你查漏补缺:
1. wait/notify/sleep/yield/join 的区别
方法 | 作用 | 是否释放锁 | 注意事项 |
---|---|---|---|
wait() | 挂起线程,等待唤醒 | 释放锁 | 必须在synchronized 块中调用 |
notify() | 唤醒对象_WaitSet 中的一个线程 |
不释放锁 | 需配合wait() 使用,唤醒后需重新竞争锁 |
sleep(long) | 让线程暂停指定时间 | 不释放锁 | 暂停期间不释放持有的同步锁 |
yield() | 让线程让出 CPU,回到就绪状态 | 不释放锁 | 不保证其他线程能获取 CPU,可能立即再次执行 |
join() | 让父线程等待子线程执行完毕 | 不释放锁 | 如主线程调用threadA.join() ,会等待threadA 执行完再继续 |
join () 的实现原理
主线程调用threadA.join()
时,会先获取threadA
对象的同步锁,然后进入等待状态;当threadA
执行完毕,会调用自身的notifyAll()
,唤醒阻塞在threadA
锁上的主线程,主线程才能继续执行。
2. 线程上下文切换:为什么会有开销?
线程切换时,CPU 需要保存当前线程的状态(上下文),再加载新线程的状态,整个过程分为 3 步:
- 挂起当前线程,将其 CPU 寄存器、程序计数器等状态存入内存;
- 从内存中加载新线程的上下文,恢复到 CPU 寄存器;
- 跳转到新线程程序计数器指向的代码行,开始执行。
切换的开销来源
- 直接消耗:CPU 寄存器的保存 / 加载、系统调度器执行、TLB(地址转换缓存)重新加载、CPU 流水线清空;
- 间接消耗:多核 CPU 中,线程切换可能导致缓存数据失效,需重新从内存读取,影响性能。
3. 为什么要用多线程?
核心目标是提升硬件利用率,降低延迟、提高吞吐量:
- 单线程下,CPU 和 I/O 设备无法并行(如 CPU 计算时,I/O 空闲;I/O 读取时,CPU 空闲),利用率最高 50%;
- 多线程可让 CPU 和 I/O 并行工作(一个线程计算,一个线程处理 I/O),理论利用率可达 100%;
- 多核 CPU 下,多线程能充分利用多个核心,大幅提高计算效率;单核下虽有切换开销,但仍能提升 I/O 密集型任务的响应速度。
总结
多线程是并发编程的基础,从进程与线程的关系,到线程的生命周期、中断机制,再到核心方法的区别,每个知识点都需要结合场景理解。这篇笔记梳理了多线程的核心框架,后续可结合实际代码(如线程池的使用、死锁的排查)进一步深化,才能真正掌握多线程的实践技巧。