多线程

多线程基础

并发和并行

概念定义

并发(Concurrency)是指多个任务在同一时间段内交替执行。这些任务由于共享CPU资源,实际上并不是真正的同时执行,而是CPU通过时间片轮转等机制快速地在不同任务之间切换,给用户造成了同时执行的错觉。

并行(Parallelism)是指多个任务在同一时刻同时执行。这要求计算机必须有多个处理器(CPU)或者多核处理器,每个任务分别在不同的处理器或核心上执行,是真正的同时执行。

区别对比

1.

  1. 特性 并发 (Concurrency) 并行 (Parallelism)
    核心定义 逻辑上的同时处理 物理上的同时执行
    关注点 任务交替执行的能力 任务同时执行的能力
    资源状态 共享资源,存在竞争条件 独享资源,通常无竞争
    实现前提 单核CPU即可实现 必须多核CPU才能实现
    主要目标 降低延迟,提高响应性和资源利用率 提高吞吐量,缩短计算时间
    典型问题 线程安全、死锁、竞态条件 任务分解、数据划分、负载均衡
    编程模型 多线程、异步回调、协程 多进程、MPI、MapReduce
    设计哲学 “看起来” 同时发生 “真正” 同时发生

实际应用

  1. Web服务器处理请求

    • 采用并发模型处理多个用户请求
    • 每个请求作为一个独立的任务在线程池中执行
  2. 视频处理软件

    • 采用并行计算加速视频编码解码
    • 将视频帧分配到不同CPU核心进行处理
  3. 游戏引擎

    • 物理计算采用并行处理
    • 游戏逻辑和UI渲染采用并发处理

编程考虑

  1. 并发编程需要注意:

    • 线程安全问题
    • 死锁预防
    • 资源同步
    • 上下文切换开销
  2. 并行编程需要注意:

    • 任务分解粒度
    • 负载均衡
    • 数据依赖关系
    • 硬件资源利用率
  • 并发​​指的是程序​​设计结构​​,关注的是在一段时间内,​​交替​​处理多个任务的能力,解决的是‘等待’的问题,核心挑战是​​线程安全​​。
    ​- ​并行​​指的是程序​​执行状态​​,依赖多核硬件,在同一​​时刻同时​​执行多个任务,目标是​​提高吞吐量​​,核心挑战是​​任务分解与负载均衡​​。

它们的关系是:​​并发是并行的基础,并行是并发的理想执行模式​​。一个设计良好的并发程序,在有多核硬件支持时,可以高效地并行运行。

进程和线程

通信方式

进程间(IPC)

进程彼此独立,空间地址隔离,不能直接访问内存,需要由操作系统提供访问方式。

管道pipe

单向通信,父子进程间常用ls | grep xxx

有匿名管道和命名管道

消息队列

以消息为单位进行通信,存放在内核队列里。

进程间可以异步读写消息。

缺点:有长度限制,大量消息可能阻塞。

共享内存

不经过内核拷贝,直接映射一块物理内存给多个进程使用。

速度最快,但需要配合锁/信号量来保证同步。

比如 mmap

信号

用来通知某个进程发生了事件(如 SIGKILLSIGINT)。

只能传很小的信息(基本就是编号),不是大规模数据通信手段。

信号量

严格意义上是同步工具,不直接传递数据。

常用于共享内存访问的互斥和同步。

套接字

不仅可用于不同主机之间,也可以在同一台机器上不同进程通信。

本地 Unix Domain Socket 用得很多(MySQL 客户端连接本地 MySQL)。

线程间(ITC)

线程共享同一进程的地址空间,所以通信方式比进程间简单很多,本质是 共享内存 + 同步机制

共享变量

所有线程可以直接读写进程内的全局变量、堆对象。

需要同步机制避免竞态。

锁机制

互斥锁(mutex):保证同一时刻只有一个线程访问共享资源。

读写锁(rwlock):读操作可并行,写操作独占。

自旋锁:忙等待,适合短临界区。

条件变量

配合锁使用,让线程在某个条件下等待/唤醒。

典型场景:生产者-消费者模型。

信号量

线程间的 PV 操作,控制并发量。

比如限流、连接池。

阻塞队列

在线程池、生产者-消费者模型中常用。

Java 的 BlockingQueue 就是典型例子。

事件/通知机制

比如 Java 的 CountDownLatchCyclicBarrier,或 Linux 的 eventfd

AQS(抽象队列同步器)

AQS是AbstractQueueSynchronizer,主要用于管理同步状态State和线程的排队等待CLH

AQS不实现具体的锁逻辑,而是定义了一系列通用函数,具体让子类来实现其方法

  • tryAcquire()如何获取独占锁
  • tryRelease()如何释放独占锁
  • tryAcquireShared()获取共享锁
  • tryReleaseShared()释放共享锁

同步状态:State

1
private volatile int state;
  • state = 0;表示没人占用
  • state=1;表示被某线程独占
  • state>1;表示重入次数(Semaphore)

所有对state的修改必须通过getState(),setState(),compareAndSetState()等unsafe的原子类操作。

双向等待队列:CLH双向队列

1
2
3
4
5
6
   abstract static class Node {
volatile Node prev; // initially attached via casTail
volatile Node next; // visibly nonnull when signallable
Thread waiter; // visibly nonnull when enqueued
volatile int status; // written by owner, atomic bit ops by others
}

每个等待的线程会被封装为一个Node节点,由于是双线队列,因此可以实现公平锁,可以让先排队的线程拿到刚释放的锁。

线程池

《Java开发手册》中提到的并发处理

1. 获取单例对象需要保证线程安全,其中的方法也要保证线程安全。

资源驱动类、工具类、单例工厂类都需要注意。

其中Spring管理的Bean是线程安全的吗?
Spring中默认Bean的作用域(@Scope("singleton"))为单例,但单例Bean并不是线程安全的,因为Spring只是保证了容器中只有一个Bean实例,但如果Bean内部包含了可变的成员变量,并且被多个线程访问和修改,依然可能存在线程安全问题。

所以:

  • 无状态Bean(如工具类、DAO通常无成员变量)一般是线程安全的
  • 有状态Bean(含成员变量,或成员变量随业务逻辑变化)需要开发者自己保证线程安全

解决方式:

  1. 使用无状态设计(即将成员变量限定为方法内局部变量):不包含任何成员变量
  2. 通过加锁/并发集合等手段保证线程安全:对修改状态的方法加 synchronized 或使用 ReentrantLock
  3. 使用@Scope("prototype")让每次请求都新建实例(开销大)
  4. 对于Web应用,可注入作用域为request/session的bean,每个请求或者会话创建一个单独的Bean
  5. 使用线程安全的成员变量,例如AtomicInteger, ConcurrentHashMap
  6. 使用不可变对象final修饰

Spring Bean 的作用域(Scope)
默认作用域是 singleton:整个 Spring 容器中只有一个实例(类似单例模式)

其他作用域:prototype、request、session 等

2. 创建线程或线程池时指定有意义的线程名称

3. 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

自行创建显示线程可能会导致:

  • 内存溢出(OOM):每个线程默认占用 1MB 栈空间(-Xss),10,000 线程 ≈ 10GB 内存
  • CPU 过载:频繁上下文切换(Context Switching),CPU 时间耗在调度而非业务
  • 系统僵死:操作系统无法创建更多线程(Linux 默认 ulimit -u 限制)

4. 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方

式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

  • 不允许通过Executors工厂类创建线程池,建议直接用ThreadPoolExecutor,可合理配置核心数、队列、拒绝策略等参数。
  • 线程池资源要合理释放,避免内存泄漏(如shutdown())。
  • 合理设置队列长度,避免OOM。
  • 任务实现尽量避免死循环、长时间阻塞。

2. 线程池的原理

线程池的本质是复用和管理一组线程,避免频繁创建销毁线程,提高系统性能。
核心实现类:java.util.concurrent.ThreadPoolExecutor

主要构造参数说明:

  • corePoolSize:核心线程数,线程池常驻线程个数
  • maximumPoolSize:最大线程数
  • keepAliveTime:非核心线程空闲超时回收时间
  • unit:超时单位
  • workQueue:阻塞队列,保存等待执行的任务
  • threadFactory:线程工厂,定制线程创建
  • handler:任务拒绝处理策略

各组件协作流程见下图:

1
2
3
4
5
6
新任务提交 -> 
若线程数<corePoolSize,则新建线程处理
否则入队列
队列满 ->
若线程数<maximumPoolSize,则新建线程处理
否则执行拒绝策略

1.Executors工厂方法

  • Executors.newFixedThreadPool(n): 固定大小线程池(适合CPU密集型),允许的队列长度为Integer.MAX_VALUE,可能导致OOM
  • Executors.newCachedThreadPool(): 可伸缩缓存线程池(适合短任务),允许的线程数量为Integer.MAX_VALUE,可能导致OOM
  • Executors.newSingleThreadExecutor(): 单线程池,允许的队列长度为Integer.MAX_VALUE,可能导致OOM
  • Executors.newScheduledThreadPool(n): 定时任务线程池,允许的线程数量为Integer.MAX_VALUE,可能导致OOM

建议:线上不要用Executors工厂类创建线程池,推荐自己指定参数使用ThreadPoolExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 线程池的核心参数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程超时时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory,
RejectedExecutionHandler handler // 拒绝策略
)

ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // core corePoolSize
4, // 最大线程数 maximumPoolSize
60, // 空闲线程超时时间 keepAliveTime
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(100), // 等待队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

2.ThreadPoolExecutor线程池执行流程

  • 判断核心线程数:先判断当前工作线程数是否大于核心线程数,如果结果为 false,则新建线程并执行任务。
  • 判断任务队列:如果大于核心线程数,则判断任务队列是否已满?如果结果为 false,则把任务添加到任务队列中等待线程执行。
  • 判断最大线程数:如果任务队列已满,则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务。
  • 判断是否要执行拒绝策略:如果超过最大线程数,则将执行线程池的拒绝策略。

3.创建核心线程数为0的线程池会怎么样执行?

在标准流程里面看起来核心线程数为0的线程池会放入阻塞队列中,如果队列是无界队列,则这些任务始终不会执行,但是实际上为了防止这种情况,在代码中会创建一个非核心线程以防永远阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0) // <- 实际上还是创建了一个非核心线程
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);

4.如何实现线程池监控?

需要普罗米修斯加spring-boot-admin

5.如何动态调整线程池的核心线程数?


多线程
https://yicizhang00.github.io/posts/编程语言/Java/并发/多线程/
作者
Yici Zhang
发布于
2025年8月12日
许可协议