10 分钟学会使用 Java 多线程

2023-12-07 14:44:17 浏览数 (2)

大家好,我是伍六七。

今天阿七来聊聊 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 了。

我们看看源码:

代码语言:javascript复制
// 是否使用 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,否则,创建普通线程执行。

代码语言:javascript复制
cpu 核数 = Runtime.getRuntime().availableProcessors();

我们要知道 CompletableFuture 获取返回是阻塞的,那我们在执行 IO 操作的时候,如果我们直接使用默认的线程池,有很大概率是会阻塞其他操作的。

所以,我们使用 CompletableFuture 的时候,如果执行 CPU 操作,可以使用默认线程池。

但是,如果执行的是 IO 操作,比如 DB 增删改查、接口调用等,尽量使用自定义线程池。

三、自定义线程池

有些情况,我们需要做到资源隔离,比如上面使用 进行 IO 操作,我们需要自定义线程池,那我们怎么定义呢?

3.1 ThreadPoolExecutor

我们可以使用 ThreadPoolExecutor,指定核心参数进行线程吹创建。

代码语言:javascript复制
ThreadPoolExecutor cutomerPoolExecutor = new ThreadPoolExecutor(10,
                10,
                1,
                TimeUnit.DAYS,
                new ArrayBlockingQueue<>(1000),
                new NamedThreadFactory("business-operation-"));

创建好之后,我们就可以往里面放任务了!我们来看个例子:首先,创建一个任务:

代码语言:javascript复制
// 测试任务,sleep 1s,模拟执行耗时任务
public class TestTask implements Runnable {
@Override
    public void run() {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

把任务放到线程池中,直接 submit 即可。

代码语言:javascript复制
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 密集型任务,然后分别用不同的线程池去处理。

只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。

因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

你学会了嘛?学会了点个赞再走~

0 人点赞