线程学习笔记
# 1 基本概念与术语
# 1.1 进程与线程
1.基础定义
进程是操作系统进行资源分配和管理的最小独立单元,而线程则是CPU调度与执行的最小单位。二者存在明确的包含关系:一个进程由一个或多个协同工作的线程构成。
2.核心差异
隔离性:进程拥有完全独立的地址空间(包括代码段、数据段和堆栈),进程间切换需要保存/恢复完整的上下文环境,因而产生较高的系统开销。线程共享所属进程的代码和全局数据,但每个线程维护独立的调用栈和程序计数器(PC),这使得线程上下文切换仅需保存少量寄存器状态,显著降低开销。
执行本质:进程作为资源容器,承载着正在运行的应用程序;线程则是该容器内的实际执行单元。现代操作系统通过时间片轮转机制,使多线程呈现出”并发执行”的假象(如视频应用中视频解码线程与用户交互线程的并行处理)。
3.稳定性特征
进程具备强隔离性,单个进程崩溃通常不会引发系统级连锁反应;而线程作为共享内存空间的执行流,某个线程的未捕获异常可能导致整个进程终止。
4.Java实现特性
在Java虚拟机中,线程作为程序执行的最小调度单元,虽然共享进程的堆内存和方法区等资源,但每个线程都拥有独立的程序计数器、虚拟机栈和本地方法栈,这种设计既保证了执行流的独立性,又实现了高效的内存资源共享。
# 1.2 并行与并发
串行:在同一时刻,有一个任务在单个CPU上依次执行。
并行:在同一时刻,有多个任务在多个CPU上同时执行。
并发:在同一时刻,有多个任务在单个CPU上交替执行。
# 1.3 线程调度
# 1.3.1 线程并发执行
计算机中的CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行代码。各个线程轮流获得CPU的使用权,分别执行各自的任务。
# 1.3.2 线程调度模型
分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU时间片。 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选取一个,优先级高的线程获取CPU的时间片相对多一些。
# 1.4 可重入锁
JVM允许同一个线程重复获取同一个锁,被同一个线程反复获取的锁,叫做可重入锁。
# 1.5 死锁
线程死锁,是由于两个或更多的线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。
死锁是因为锁的嵌套产生的,所以避免死锁的根本就是要避免锁的嵌套。多线程获取锁的顺序要一致。
# 2 创建线程的方式
# 2.1 继承Thread类
继承Thread类,并重写run方法。
# 2.2 实现Runnable接口
实现Runnable接口,并重写run方法。
# 2.3 实现Callable接口
实现Callable接口,并重写call方法。
call方法有返回值的,可以抛出异常。Callable接口支持泛型。实现Callable接口的派生类需要结合FutureTask一起使用。
JDK5新增的。
- Future可以对Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask是Future接口唯一的实现类
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
# 2.4 线程池
# 2.4.1 创建线程池
# 2.4.2 使用线程池
# 2.4.3 线程池参数
创建自定义线程池时需要设置以下核心参数:
- **核心线程数(corePoolSize)**:线程池初始化时创建的线程数量
- **最大线程数(maxPoolSize)**:线程池允许的最大线程数量
- **队列容量(queueCapacity)**:任务缓存队列的大小
- **线程空闲时间(keepAliveSeconds)**:超出核心线程数的线程空闲时被销毁的等待时间
- **线程名前缀(threadNamePrefix)**:便于监控和调试
- **拒绝策略(rejectedExecutionHandler)**:当线程池和队列都满时的处理方式
# 2.4.4 线程池拒绝策略
线程池的拒绝策略在任务提交数超过(最大线程数 + 队列容量)时被触发。
JDK内置了四种拒绝策略:
- AbortPolicy:默认,丢弃任务并抛出异常。适用于需要立即感知任务被拒绝的场景,通过异常提醒系统过载。
- CallerRunsPolicy:由提交任务的线程自行执行该任务。适用于不能丢弃任何任务,但能接受提交线程性能下降的场景。
- DiscardPolicy:丢弃任务但不抛出异常。适用于可容忍任务丢失且不希望抛出异常的场景。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重试提交当前任务。适用于希望尝试执行最新任务的场景。
当内置策略不满足需求时,可以实现RejectedExecutionHandler接口来自定义策略。例如,以下自定义策略在丢弃任务前会记录日志:
public class CustomDiscardPolicy implements RejectedExecutionHandler {
private String factoryName;
public CustomDiscardPolicy(String factoryName) {
this.factoryName = factoryName;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
Runnable poll = executor.getQueue().poll(); // 丢弃队列头部任务
System.err.println("[" + this.factoryName + "] task will be discard: " + poll);
executor.execute(r); // 重试提交当前任务
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.4.5 线程池配置和使用误区
在线程池配置和使用过程中,确实存在一些常见的误区,这些误区可能导致内存溢出、性能下降甚至系统崩溃。
使用Executors快捷创建线程池:直接使用Executors的newFixedThreadPool、newCachedThreadPool等方法创建线程池是常见的错误。这些方法创建的线程池要么使用无界队列(如LinkedBlockingQueue),任务无限堆积可能导致内存溢出(OOM);要么允许创建的线程数量为Integer.MAX_VALUE,可能耗尽系统资源。正确的做法是手动通过ThreadPoolExecutor的构造函数来声明线程池,明确指定核心线程数、最大线程数、队列类型及容量、拒绝策略等参数。
线程池参数设置不合理:随意配置线程池参数是另一个常见问题。例如,核心线程数设置过小可能导致任务处理不及时,而设置过大又可能造成资源浪费;最大线程数设置过高,在突发流量下可能创建过多线程,同样有耗尽资源的风险。合理的配置需要根据机器的性能、业务场景(如CPU密集型或I/O密集型)来调整。
线程池拒绝策略选择不当:如果未根据业务特点选择合适的拒绝策略,当任务无法被处理时,可能会影响系统稳定性。例如,默认的AbortPolicy会直接抛出异常,而CallerRunsPolicy则会让调用线程执行任务,起到一定的缓冲作用。
共享线程池的风险:在项目中共享同一个线程池处理所有异步任务存在风险。如果次要逻辑的任务执行缓慢或阻塞,可能会占用大量线程资源,从而拖垮主要逻辑,导致系统性能下降。
线程池中的异常丢失:在线程池中执行任务时,如果任务内部发生了异常但没有被捕获,这个异常可能会“消失”,导致问题难以追踪和排查。
线程池命名与监控缺失:使用线程池时没有自定义命名,会给问题排查带来困难。同时,缺乏对线程池运行状态的监控(如活跃线程数、队列长度),使得无法及时发现潜在的性能问题或资源瓶颈。
ThreadLocal与线程池搭配的风险:由于线程池会复用线程,如果在任务中使用了ThreadLocal,并且在使用后没有及时清理,可能会导致后续任务获取到错误的信息,造成数据混乱。
线程池忘记关闭:在一些需要手动管理线程池生命周期的场景下,使用完线程池后忘记调用shutdown方法,可能导致线程无法被回收,造成资源泄漏。
# 3 线程的生命周期

线程从创建到销毁会经历以下 5 种核心状态,其转换关系如下:
新建(NEW)
- 触发条件:通过
new关键字创建Thread对象,但尚未调用start()方法。 - 特点:线程对象已存在,但尚未进入调度队列,不占用 CPU 资源。
- 触发条件:通过
就绪(RUNNABLE)
- 触发条件:
- 调用
start()方法,线程进入可运行状态; - 从 阻塞(BLOCKED) 或 等待(WAITING/TIMED_WAITING) 状态恢复(如
sleep()时间到、I/O 操作完成、锁获取成功)。
- 调用
- 特点:线程已准备好执行,等待 CPU 调度,但尚未真正运行。
- 触发条件:
运行(RUNNING)
- 触发条件:线程调度器(OS)选中该线程,并分配 CPU 时间片。
- 特点:线程实际执行
run()方法中的代码,是 RUNNABLE 状态的子集(即只有获得 CPU 的 RUNNABLE 线程才处于 RUNNING 状态)。
阻塞(BLOCKED / WAITING / TIMED_WAITING)
- 触发条件:
- BLOCKED:线程尝试获取 对象锁(synchronized) 但锁被其他线程占用;
- WAITING:调用
wait()、join()等方法,线程进入无限期等待,直到被notify()/notifyAll()唤醒; - TIMED_WAITING:调用
sleep(ms)、wait(ms)、join(ms)等带超时的方法,线程进入限时等待。
- 特点:线程暂停执行,不占用 CPU,直到满足特定条件(如锁释放、超时、外部唤醒)。
- 触发条件:
死亡(DEAD / TERMINATED)
- 触发条件:
run()方法执行完毕;- 线程抛出未捕获的异常;
- 调用
stop()(已废弃,不推荐使用)。
- 特点:线程生命周期结束,无法再次启动(
start()会抛出IllegalThreadStateException)。
- 触发条件:
# 4 线程的安全问题
解决线程之间共享数据的安全问题有以下3种方式。
# 4.1 使用volatile
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
public volatile boolean myVar = true;
# 4.2 使用synchronized
同步代码块
synchronized(lock){
// 业务逻辑
}
2
3
同步方法
public synchronized void myMethod(){
// 业务逻辑
}
2
3
# 4.3 使用ReentrantLock
Lock,JDK5新增的
private final Lock lock = new ReentrantLock();
try{
lock.lock();
// 业务逻辑
}
finally{
lock.unlock();
}
2
3
4
5
6
7
8
9
# 4.4 几种方式比较
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。
ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
# 5 线程通信的应用
生产者消费者模式,也叫等待唤醒机制,是一个非常经典的多线程协作模式。
一个线程负责生产数据,放到共享区域,然后通知另一个线程去消耗数据。
synchronized + wait + notify 实现多线程协调
public synchronized void send() throws InterruptedException { if(条件){ // 业务逻辑 notify(); } else { wait(); } }1
2
3
4
5
6
7
8
9Lock + Condition 实现多线程协调
private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void send() throws InterruptedException { try{ lock.lock(); if(条件){ // 业务逻辑 condition.signal(); // 相当notify } else { condition.await(); // 相当于wait } } finally{ lock.unlock(); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的
# 6 扩展知识
# sleep 和 wait 的区别
相同点:
- 调用sleep或wait,线程都会进入阻塞状态。
不同点:
- 声明位置不同:Thread类中声明sleep,Object类中声明wait;
- 调用位置不同:sleep在任何地方都可以使用,wait只能用在同步代码块或者同步方法中;
- 关于是否释放同步监听器:如果两个方法都用在同步代码块或者同步方法中,sleep不会释放,wait会释放。
# wait、notify、notifyAll
三个方法都是定义在Object类中;
三个方法只能使用在同步代码块或同步方法中;
三个方法的调用者必须是同步代码块或同步方法中的同步监听器;
一旦调用wait方法,线程就会进入阻塞状态,释放同步监听器;
一旦调用notify方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级最高的那个;
一旦调用notifyAll方法,就会唤醒所有被wait的线程。
# 参考资料
多线程 - 廖雪峰的官网网站 (opens new window)
Java -Java 学习- Java 基础到高级-宋红康-零基础自学Java-尚硅谷 (opens new window)
Java中Atomic类的使用分析 (opens new window)
锁机制
锁升级
偏向锁、轻量级锁、重量级锁
AQS是什么?AQS如何实现可重入锁?
可重入锁
可重入锁,也称递归锁。
可以重复获取相同的锁。
https://blog.csdn.net/w8y56f/article/details/89554060
如果一个线程在执行一个持有锁的方法,在这个方法中调用另一个持有相同锁的方法,则该线程可以直接调用,而无需重新获取锁。
2
3
4
5
6