线程学习笔记

2025/10/21 JavaExecutor线程池

# 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);                          // 重试提交当前任务
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2.4.5 线程池配置和使用误区

在线程池配置和使用过程中,确实存在一些常见的误区,这些误区可能导致内存溢出、性能下降甚至系统崩溃。

使用Executors快捷创建线程池:直接使用ExecutorsnewFixedThreadPoolnewCachedThreadPool等方法创建线程池是常见的错误。这些方法创建的线程池要么使用无界队列(如LinkedBlockingQueue),任务无限堆积可能导致内存溢出(OOM);要么允许创建的线程数量为Integer.MAX_VALUE,可能耗尽系统资源。正确的做法是手动通过ThreadPoolExecutor的构造函数来声明线程池,明确指定核心线程数、最大线程数、队列类型及容量、拒绝策略等参数。

线程池参数设置不合理:随意配置线程池参数是另一个常见问题。例如,核心线程数设置过小可能导致任务处理不及时,而设置过大又可能造成资源浪费;最大线程数设置过高,在突发流量下可能创建过多线程,同样有耗尽资源的风险。合理的配置需要根据机器的性能、业务场景(如CPU密集型或I/O密集型)来调整。

线程池拒绝策略选择不当:如果未根据业务特点选择合适的拒绝策略,当任务无法被处理时,可能会影响系统稳定性。例如,默认的AbortPolicy会直接抛出异常,而CallerRunsPolicy则会让调用线程执行任务,起到一定的缓冲作用。

共享线程池的风险:在项目中共享同一个线程池处理所有异步任务存在风险。如果次要逻辑的任务执行缓慢或阻塞,可能会占用大量线程资源,从而拖垮主要逻辑,导致系统性能下降。

线程池中的异常丢失:在线程池中执行任务时,如果任务内部发生了异常但没有被捕获,这个异常可能会“消失”,导致问题难以追踪和排查。

线程池命名与监控缺失:使用线程池时没有自定义命名,会给问题排查带来困难。同时,缺乏对线程池运行状态的监控(如活跃线程数、队列长度),使得无法及时发现潜在的性能问题或资源瓶颈。

ThreadLocal与线程池搭配的风险:由于线程池会复用线程,如果在任务中使用了ThreadLocal,并且在使用后没有及时清理,可能会导致后续任务获取到错误的信息,造成数据混乱。

线程池忘记关闭:在一些需要手动管理线程池生命周期的场景下,使用完线程池后忘记调用shutdown方法,可能导致线程无法被回收,造成资源泄漏。

# 3 线程的生命周期

线程从创建到销毁会经历以下 5 种核心状态,其转换关系如下:

  1. 新建(NEW)

    • 触发条件:通过 new 关键字创建 Thread 对象,但尚未调用 start() 方法。
    • 特点:线程对象已存在,但尚未进入调度队列,不占用 CPU 资源。
  2. 就绪(RUNNABLE)

    • 触发条件
      • 调用 start() 方法,线程进入可运行状态;
      • 阻塞(BLOCKED)等待(WAITING/TIMED_WAITING) 状态恢复(如 sleep() 时间到、I/O 操作完成、锁获取成功)。
    • 特点:线程已准备好执行,等待 CPU 调度,但尚未真正运行。
  3. 运行(RUNNING)

    • 触发条件:线程调度器(OS)选中该线程,并分配 CPU 时间片。
    • 特点:线程实际执行 run() 方法中的代码,是 RUNNABLE 状态的子集(即只有获得 CPU 的 RUNNABLE 线程才处于 RUNNING 状态)。
  4. 阻塞(BLOCKED / WAITING / TIMED_WAITING)

    • 触发条件
      • BLOCKED:线程尝试获取 对象锁(synchronized) 但锁被其他线程占用;
      • WAITING:调用 wait()join() 等方法,线程进入无限期等待,直到被 notify()/notifyAll() 唤醒;
      • TIMED_WAITING:调用 sleep(ms)wait(ms)join(ms) 等带超时的方法,线程进入限时等待。
    • 特点:线程暂停执行,不占用 CPU,直到满足特定条件(如锁释放、超时、外部唤醒)。
  5. 死亡(DEAD / TERMINATED)

    • 触发条件
      • run() 方法执行完毕;
      • 线程抛出未捕获的异常;
      • 调用 stop()(已废弃,不推荐使用)。
    • 特点:线程生命周期结束,无法再次启动(start() 会抛出 IllegalThreadStateException)。

# 4 线程的安全问题

解决线程之间共享数据的安全问题有以下3种方式。

# 4.1 使用volatile

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

public volatile boolean myVar = true;
1

# 4.2 使用synchronized

同步代码块

synchronized(lock){
    // 业务逻辑
}
1
2
3

同步方法

public synchronized void myMethod(){
 // 业务逻辑
}
1
2
3

# 4.3 使用ReentrantLock

Lock,JDK5新增的

private final Lock lock = new ReentrantLock();

try{
    lock.lock();
    // 业务逻辑
}
finally{
    lock.unlock();
}
1
2
3
4
5
6
7
8
9

# 4.4 几种方式比较

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。

ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

# 5 线程通信的应用

生产者消费者模式,也叫等待唤醒机制,是一个非常经典的多线程协作模式

一个线程负责生产数据,放到共享区域,然后通知另一个线程去消耗数据。

  1. synchronized + wait + notify 实现多线程协调

    public synchronized void send() throws InterruptedException {
     if(条件){
            // 业务逻辑
      notify();
     }
     else {
      wait();
     }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  2. Lock + 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,线程都会进入阻塞状态。

不同点:

  1. 声明位置不同:Thread类中声明sleep,Object类中声明wait;
  2. 调用位置不同:sleep在任何地方都可以使用,wait只能用在同步代码块或者同步方法中;
  3. 关于是否释放同步监听器:如果两个方法都用在同步代码块或者同步方法中,sleep不会释放,wait会释放。

# wait、notify、notifyAll

  1. 三个方法都是定义在Object类中;

  2. 三个方法只能使用在同步代码块或同步方法中;

  3. 三个方法的调用者必须是同步代码块或同步方法中的同步监听器;

  4. 一旦调用wait方法,线程就会进入阻塞状态,释放同步监听器;

  5. 一旦调用notify方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级最高的那个;

  6. 一旦调用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

如果一个线程在执行一个持有锁的方法,在这个方法中调用另一个持有相同锁的方法,则该线程可以直接调用,而无需重新获取锁。
1
2
3
4
5
6