线程池相关知识及并发统计案例实现

引言

今天我们来学习线程池相关的内容。那么以一个基于实际的 Java 多线程统计案例为学习切入点,让我们详细分析线程池的使用方式、任务拆分思想、并发统计原理,以及代码中涉及到的重要知识点


案例引入

现在有一个长度为100万的字符串数组(都是字母) 我们想要统计其中每个字符串,最后输出字母a的个数(后续可扩展到每一个字母)
首先我们来分析一下,为什么这个案例题目需要用到线程池:


线程池原理

如果按照常规思路,每次执行统计任务的时候都

复制代码
new Thread().start();

创建一个线程,那么会出现以下几个问题:

1.为什么需要线程池

1).线程创建开销十分巨大:因为线程属于系统资源,每次创建线程都需要分配内存,创建线程对象同时调用系统内核。那么面对大数据时,频繁创建销毁线程非常的消耗性能。

2).线程数量太多导致不可控:面对大数据时大量线程的瞬间创建,直接会导致内存暴涨、系统卡死。

3).线程无法复用:每次任务执行完线程自然死亡,新的任务又要重新创建线程,导致资源浪费十分严重。

2.线程池的核心原理

本质:

  • 提前创建好线程
  • 任务来了直接复用线程

(就比如工厂提前招聘工人,在有订单时直接干活而不用临时招人)

那么这样的方式就会有其明显的优点:

优点:

  • 降低线程创建开销
  • 控制线程数量
  • 提高系统稳定性
  • 提高并发性能

了解后使用原因后,我们可以为解决案例设计以下整体流程

主线程

创建 NetworkService

创建线程启动 service.run()

创建线程监控线程池状态

线程池执行多个 Handler 任务

每个 Handler 统计一部分字符串中的 a

使用 AtomicInteger 汇总结果

线程池关闭

输出最终统计结果

(而该设计的核心思路本质上是并行计算的经典思想,即

将大任务拆分为多个小任务,通过线程池并发执行,最后汇总结果)

好的下面我们逐步分析整个流程

首先让我们


创建线程池

复制代码
pool = new ThreadPoolExecutor(
        poolSize,
        poolSize * 2,
        30,
        java.util.concurrent.TimeUnit.MILLISECONDS,
        new java.util.concurrent.ArrayBlockingQueue<>(100)
);

1. ThreadPoolExecutor 构造参数

构造方法:

复制代码
ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    unit,
    workQueue
)

2. 参数解析

(1)corePoolSize

复制代码
poolSize//核心线程数

new NetworkService(10);

因此:

复制代码
核心线程数 = 10

含义:

  • 线程池最少保留10个线程
  • 即使空闲也不会销毁

(2)maximumPoolSize

复制代码
poolSize * 2

复制代码
最大线程数 = 20

含义:

  • 当任务很多时
  • 线程池最多扩展到20个线程

(3)keepAliveTime

复制代码
30

含义:

复制代码
非核心线程空闲30ms后销毁

(4)TimeUnit.MILLISECONDS

时间单位:

复制代码
毫秒

(5)ArrayBlockingQueue

复制代码
new ArrayBlockingQueue<>(100)

阻塞队列。

作用:存储等待执行的任务

容量:最多100个任务


测试数据初始化

为了方便测试,我们先设置小数据

复制代码
String[] str = new String[10];

初始化:

复制代码
// 测试数据
        str[0] = "aaa";
        str[1] = "abc";
        str[2] = "bbb";
        str[3] = "aac";
        str[4] = "ddd";
        str[5] = "a";
        str[6] = "xyz";
        str[7] = "aa";
        str[8] = "hello";
        str[9] = "javaaaaaaaaaaaaa";

作用:用于模拟待处理的数据。

run() 方法核心分析

1. CPU核心数

复制代码
int cpuNums = 32;

这里模拟:

复制代码
CPU 有 32 个核心

2. 为什么任务数是 cpuNums * 2?

代码:

复制代码
int taskNum = Math.min(cpuNums * 2, str.length);

即:

复制代码
任务数 = CPU核心数 × 2

原因:

CPU执行线程时,线程可能需要计算,io流等待,sleep,阻塞等,

如果只创建:

复制代码
//任务数
int taskNum = cpuNums * 2;

CPU核心数 = 32
线程数 = 32

那么可能部分线程在等待。

因此通常,我们使线程数>CPU核心数来提高CPU利用率。

问题1:

但是我一开始在小数据情况下仅考虑

复制代码
//任务数
int taskNum = cpuNums * 2;

×

**错误描述:**运行跑起来后发现卡卡的,且有好多用于输出的代码上,前面提到的参数corePoolSize以及maximumPoolSize疯狂跑0,只发现最后一次出现较为有效的输出。

解决方案:

需要对分片任务数做出以下限制:

复制代码
//分片任务数不能超过字符串数组的长度
int taskNum = Math.min(cpuNums * 2, str.length);

因为小字符字符串只有10个,没必要创建64个任务导致空跑,浪费资源


任务拆分思想(重点)

1. 分片大小计算

很好理解,分片大小的计算就=总字符串的长度/任务数呗,于是我直接

问题2又来了:

复制代码
//分片大小
int chunkSize = str.length / taskNum;

×

**错误描述:**导致所有的参数都少跑或直接跑0。。。。。

解决方案:注意向上取整。。不然极容易取到0;

复制代码
//分片大小向上取整
int chunkSize = (str.length + taskNum - 1) / taskNum;

使用经典的向上取整公式


任务区间划分

复制代码
for (int i = 0; i < taskNum; i++) {

每轮创建一个任务。


1. start计算

复制代码
int start = i * chunkSize;

例如:

i start
0 0
1 1
2 2

2. end计算

复制代码
if (i == taskNum - 1) {
    end = str.length;
} else {
    end = (i + 1) * chunkSize;
}

最后一个任务负责处理剩余所有数据


因此我们得到实际划分结果:

任务 区间
0 [0,1)
1 [1,2)
2 [2,3)
... ...
9 [9,10)

线程池提交任务

复制代码
pool.execute(new Handler(str, start, end, totalCount));

含义:将任务提交给线程池,那么线程池就会从线程池中取出空闲的线程执行任务


shutdown() 原理

复制代码
pool.shutdown();

注意:

复制代码
shutdown() ≠ 立即关闭线程池

它的本质其实是不再接收新任务但是会执行完接收到的已有任务

因此程序依旧会继续执行。


Handler 类分析

1. Handler 的作用

每个 Handler负责处理一部分字符串

统计 a 的核心逻辑

复制代码
for (int i = start; i < end; i++) {

遍历当前任务负责的数据。


字符遍历

复制代码
for (int j = 0; j < s.length(); j++) {

遍历字符串字符。


判断字符

复制代码
if (s.charAt(j) == 'a')

判断是否为 'a'


局部统计

复制代码
count++;

注意:

这里使用局部变量:

复制代码
int count = 0;

原因:

局部变量属于线程私有,不会导致线程冲突


AtomicInteger 线程安全原理(重点)

1. 为什么不用普通 int?

如果:

复制代码
int totalCount;

多个线程同时:

复制代码
totalCount++;

肯定会产生线程安全问题:

  • 因为totalCount++不是原子操作
  • 多个线程同时操作读写,计数++,肯定会导致数据覆盖,最终统计错误。

2. AtomicInteger 原理

复制代码
AtomicInteger totalCount = new AtomicInteger(0);

它基于**CAS(Compare And Swap)**实现无锁原子操作。


3. addAndGet()

复制代码
totalCount.addAndGet(count);

作用:实现原子性的累加,即多个线程同时操作也不会出错


监控线程池状态

复制代码
pool.getActiveCount()

获取:当前活跃线程数

复制代码
pool.getPoolSize()

获取:当前线程池总数

复制代码
pool.getTaskCount()

获取:总任务数

复制代码
pool.getCompletedTaskCount()

获取:已完成任务数


本案例并发思想总结

本案例体现了经典:

"分治 + 并发"思想。

即:

复制代码
大任务
→ 拆分成多个小任务
→ 多线程并发执行
→ 汇总结果

涉及核心知识点

并发相关

  • 多线程
  • Runnable
  • 线程池
  • ThreadPoolExecutor
  • 阻塞队列
  • 原子类
  • CAS
  • 线程安全

算法思想

  • 分片处理
  • 向上取整
  • 分治思想

程序最终统计结果分析

运行结果:

统计所有字符串中的 'a'

复制代码
"aaa" → 3
"abc" → 1
"bbb" → 0
"aac" → 2
"ddd" → 0
"a" → 1
"xyz" → 0
"aa" → 2
"hello" → 0
"javaaaaaaaaaaaaa" → 14

最终:

复制代码
总数 = 23

总结

这个案例虽然规模不大,但让我完整学习了:

  • 线程池使用
  • 任务拆分
  • 并发处理
  • 原子操作
  • 线程池监控

等核心并发知识。

相关推荐
流年如夢1 小时前
初入C++
开发语言·c++
zzzsde1 小时前
【Linux】线程同步和互斥(1):线程互斥与加锁实现
linux·运维·服务器·开发语言·算法
yoyo_zzm1 小时前
编程语言大比拼:C++到PHP全解析
开发语言·c++·php
努力努力再努力wz1 小时前
【C++高阶数据结构系列】:时间轮定时器详解:原理分析与代码实现,带你从零手撕时间轮!(附时间轮的实现源码)
c语言·开发语言·数据结构·c++·qt·算法·ui
慕言手记2 小时前
IDEA 插件常用-2026版
java·ide·spring boot·intellij-idea·idea·intellij idea
颖火虫盟主2 小时前
Hello World MCP Server 实现总结
java·前端·python
Chen_harmony2 小时前
十九、数据在内存中的存储
c语言·开发语言
basketball6162 小时前
C 的 malloc/free 与 C++ 的 new/delete 一些区别
c语言·开发语言·c++
iiiiyu2 小时前
⾯向对象和集合编程题
java·大数据·开发语言·数据结构·编程语言