Java线程池-1

总所周知,创建线程比较损耗资源,频繁创建线程容易造成性能降低。因此需要使用线程池,主要原因是可以复用线程,另外使用线程池也可以比较方便的对线程进行管理。下面,让我们一起来看下在Java中如何使用线程池。

ThreadPoolExecutor

ThreadPoolExecutor是Java线程池的具体实现,首先来看下ThreadPoolExecutor的相关参数。

corePoolSize

核心线程数,怎么理解这个参数呢,重点在于核心两个字。为了节省资源,线程池会回收处于空闲状态的线程,但是线程池不会回收核心线程,即使该线程处于空闲状态。

maximumPoolSize

最大线程数,表示的是线程的容量,也就是线程池最多可以创建多少个线程。

keepAliveTime

线程存活时间,当线程空闲的时间超过存活时间,则这个线程会被回收

workQueue

当提交的任务超过线程池的处理能力,也就是所需的线程超过maximumPoolSize,提交的任务会被放入等待队列中。

threadFactory

线程工厂,线程池通过这个创建线程

RejectedExecutionHandler

拒绝策略,当workQueue的容量也被占满时,处理新提交的任务的策略。Java定义了4种策略

Policy explain
AbortPolicy 抛出RejectedExecutionException异常
CallerRunsPolicy 在调用方(caller)的线程中运行该任务
DiscardOldestPolicy 丢弃等待最久的任务,并在次提交新任务
DiscardPolicy 丢弃新提交的任务,不做任何处理

如果上述几种策略都不满足需求,我们可以自定义策略,只要实现RejectedExecutionHandler接口即可

小结

前文介绍了创建一个线程池所需的相关参数,为了方便理解,在这里类比一个不是很恰当的例子。

假设有一家医院(线程池),总有有10(maximumPoolSize)个医生,平时都会有5(corePoolSize)个医生值班,当病人较多的时候,其他几位医生也会被叫回医院上班,当病人处理完时,这几位被临时叫回的医生又可以下班了,但是值班的5位医生还是得呆在医院。

如果发生突发情况,医院的10位医生都来不及救治的病人,那么病人只好在候诊室(workQueue)等着,当医生处理完手头上的病人时,会从候诊室叫一位病人过来就诊。但是医院的候诊室也是有限制,当候诊人数达到上限时,医院只好劝退(RejectedExecutionHandler)新来的病人

内置线程池

CachedThreadPool


可以看到corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,说明当任务提交,若没有可用的线程,会立马新建线程。

CachedThreadPool比较适合大量执行时间较短的任务,因为当任务执行时间较长时,空闲的线程较少,新任务出现时只能创建新的线程,而线程池的maximumPoolSize又为Integer.MAX_VALUE,这样会导致线程池创建大量的线程,从而可能出现OOM

FixedThreadPool


FixedThreadPool的corePoolSize=maximumPoolSize,说明线程池的线程数是固定的。当任务数超过线程池容量时,任务将被放入等待队列中。

FixedThreadPool比较适合任务数可预估并且相对不变的情况。

SingleThreadExecutor


SingleThreadExecutor的corePoolSize=maximumPoolSize=1,线程池只存在一个线程,同一时刻只有一个任务被执行,其他任务都将放入等待队列中

小结

前文介绍Java内置的几种线程池,但是在实际应用,不推荐使用这几种线程池,而是推荐使用ThreadPoolExecutor来创建线程池,这是为什么呢?

通过观察上述几种线程池的参数,我们发现他们等待队列的容量是没有上限的,当大量任务堆积在等待队列时,容易导致OOM,因此推荐通过ThreadPoolExecutor来创建线程池,这样线程的创建会相对可控。实际上我们也看到,内置的几种线程池也是通过ThreadPoolExecutor来生成的。

测试

下面我们来运行几个例子,以便加深理解

/**
 * @author wbl
 * @date 2020-02-04
 */
public class ThreadPoolTest {
    public static void main(String[] args) throws InterruptedException {
        // 核心线程数
        int corePoolSize = 5;
        // 线程池容量
        int maximumPoolSize = 10;
        // 线程存活时间,单位秒
        long keepAliveTime = 1;
        // 等待队列容量
        int queueSize = 10;
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize,
                maximumPoolSize,
                keepAliveTime, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(queueSize),
                new ThreadPoolExecutor.DiscardPolicy());

        // 任务数
        int taskCount = 50;
        // 完成任务所需时间,单位为ms
        int completeTaskTime = 1000;
        for (int i = 0; i < taskCount; i++){
            poolExecutor.execute(()->{
                try {
                    Thread.sleep(completeTaskTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(String.format("%s complete task",Thread.currentThread().getName()));
            });
        }
        // 关闭线程池
        poolExecutor.shutdown();
        // 等待线程池中的任务执行完毕
        while (!poolExecutor.isTerminated()){

        }
        // 打印完成的任务数
        System.out.println("completed task:" + poolExecutor.getCompletedTaskCount());
    }
}

执行上述代码,可以得到如下结果

大家可以思考下,为什么提交的任务是50个,而完成的任务数只有20个呢?

任务的执行为1s,而任务的提交的速度远远快于这个,同时上述线程池的最大容量为10,等待队列的容量为10,也就是意味着当提交的任务超过20个时,新提交的任务将会被丢弃,因此只有20个任务会被执行。

为了验证存在任务被丢弃,我们可以修改拒绝策略

ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize,
                maximumPoolSize,
                keepAliveTime, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(queueSize),
                new ThreadPoolExecutor.AbortPolicy());

再次运行代码,可以看到程序抛出了RejectedExecutionException

大家也可以思考下,怎么修改线程池的参数也能让所有的任务执行完呢?

其实很简单,我们可以调整线程池的maximumPoolSize,让其大于提交的任务数,也可以调整等待队列的容量,让其超过提交的任务数,还可以减缓任务提交的速度,让其大于任务的执行时间。


Reprint please specify: wbl Java线程池-1

Previous
Java线程池-2 Java线程池-2
上一篇Java线程池-1中,我们了解了线程池的相关参数以及内置的几种线程池,现在我们来研究下如何自己实现一个线程池。 在实现之前,我们先思考下,一个线程池有哪些要素呢? 线程复用 线程管理 拒绝策略 线程复用如何实现线程的复用呢?还记得
2020-02-06 wbl
Next