大家好,我是伍六七。
今天阿七来聊聊 Java 程序员们面试、工作中经常会碰到的线程池。它的概念、原理、使用以及可能会碰到的一个坑。
一、Java 线程池基本概念
1、线程池的 7 个核心参数
这是 Java 初中级程序员们面试必问的面试题了,我们来看:
- corePoolSize(核心线程数)
corePoolSize 是线程池中保持活动状态的最小线程数。 即使线程是空闲的,它们也会一直保持在池中。 当有新任务提交时,线程池会优先创建核心线程来处理任务。
- maximumPoolSize(最大线程数)
maximumPoolSize 是线程池中允许的最大线程数。 如果任务数超过了核心线程数,且任务队列已满,线程池会创建新的线程,但不会超过最大线程数。
- keepAliveTime(线程空闲时间)
keepAliveTime 是非核心线程在空闲时可以存活的时间。 当线程空闲时间超过 keepAliveTime,多余的非核心线程将被终止,以减少资源消耗。
这个参数配合 TimeUnit 来定义时间单位。
- unit(时间单位):
unit 是与 keepAliveTime 一起使用的时间单位。 它表示 keepAliveTime 的时间单位,可以是秒、毫秒、微秒等。
-
workQueue(任务队列): workQueue 是一个阻塞队列,用于存储等待执行的任务。 当任务数超过核心线程数时,多余的任务会被放入任务队列中。 常见的队列类型包括 LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue 等。
-
threadFactory(线程工厂):
threadFactory 用于创建新线程。 可以通过自定义线程工厂来配置线程的名称、优先级、是否为守护线程等属性。
- handler(饱和策略)
handler 是当工作队列和线程池都满了之后采取的饱和策略。 常见的饱和策略有 AbortPolicy(默认,抛出异常)、CallerRunsPolicy(由调用线程执行任务)等。
这些参数在创建线程池时进行配置,通过合理调整这些参数,可以使线程池适应不同的工作负载和性能需求。例如,通过调整核心线程数和最大线程数,可以优化线程池在不同负载下的性能表现。
2、线程池是怎么运转的?
举例来说:核心线程数量为 5 个;全部线程数量为 10 个;工作队列的长度为 5。
刚开始都是在创建新的线程,达到核心线程数量 5 个后,新的任务进来后不再创建新的线程,而是将任务加入工作队列;
任务队列到达上线 5 个后,新的任务又会创建新的普通线程,直到达到线程池最大的线程数量 10 个;
后面的任务则根据配置的饱和策略来处理。如果没有配置,使用的是默认的配置 AbortPolicy:直接抛出异常。
当当前任务小于最大线程数的时候,线程资源会保持核心线程池个数的线程,其他超过的线程资源在存活时间时间之后会被回收。
二、Future 关键字
我们在项目中会经常使用 CompletableFuture 执行异步任务。那你知道 CompletableFuture 使用的是什么线程池吗?这个线程池适合执行什么类型的任务呢?
之前阿七刚转到互联网的时候,就因为使用 CompletableFuture 不当,被领导 diss 了。
我们看看源码:
java
// 是否使用 useCommonPool,如果(cpu 的核数 -1)大于 1,使用 ForkJoinPool,否则,不使用线程池。
private static final boolean useCommonPool =
(ForkJoinPool.getCommonPoolParallelism() > 1);
/**
* Default executor -- ForkJoinPool.commonPool() unless it cannot
* support parallelism.
*/
// 使用线程池还是创建普通线程
private static final Executor asyncPool = useCommonPool ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
/** Fallback if ForkJoinPool.commonPool() cannot support parallelism */
static final class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) { new Thread(r).start(); }
}
我们看到,默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数
PS:也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数。
但是也不一定就使用 ForkJoinPool,要看(cpu 的核数 -1)是否大于 1,如果大于 1,使用过 ForkJoinPool,否则,创建普通线程执行。
scss
cpu 核数 = Runtime.getRuntime().availableProcessors();
我们要知道 CompletableFuture 获取返回是阻塞的,那我们在执行 IO 操作的时候,如果我们直接使用默认的线程池,有很大概率是会阻塞其他操作的。
所以,我们使用 CompletableFuture 的时候,如果执行 CPU 操作,可以使用默认线程池。
但是,如果执行的是 IO 操作,比如 DB 增删改查、接口调用等,尽量使用自定义线程池。
三、自定义线程池
有些情况,我们需要做到资源隔离,比如上面使用 进行 IO 操作,我们需要自定义线程池,那我们怎么定义呢?
3.1 ThreadPoolExecutor
我们可以使用 ThreadPoolExecutor,指定核心参数进行线程吹创建。
java
ThreadPoolExecutor cutomerPoolExecutor = new ThreadPoolExecutor(10,
10,
1,
TimeUnit.DAYS,
new ArrayBlockingQueue<>(1000),
new NamedThreadFactory("business-operation-"));
创建好之后,我们就可以往里面放任务了!我们来看个例子: 首先,创建一个任务:
java
// 测试任务,sleep 1s,模拟执行耗时任务
public class TestTask implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
把任务放到线程池中,直接 submit 即可。
arduino
personalPoolExecutor.submit(new TestTask());
3.2 怎么设置线程池的参数?
线程池究竟设成多大是要看你给线程池处理什么样的任务,任务类型不同,线程池大小的设置方式也是不同的。
任务一般可分为:CPU 密集型、IO 密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。
- CPU 密集型任务
尽量使用较小的线程池,一般为 CPU 核心数 +1。 因为 CPU 密集型任务使得 CPU 使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。
- IO 密集型任务
可以使用稍大的线程池,一般为 2*CPU 核心数。 IO 密集型任务 CPU 使用率并不高,因此可以让 CPU 在等待 IO 的时候去处理别的任务,充分利用 CPU 时间。
- 混合型任务
最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)] 可以将任务分成 IO 密集型和 CPU 密集型任务,然后分别用不同的线程池去处理。
只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
你学会了嘛?学会了点个赞再走~
关注我,送你全套我整理的 Java 岗位面试资料。这是我自己之前整理的面试题,靠着这份面试题,我从 30 人的小公司,进了 2000 人+的央企子公司,之后又进了互联网大厂。