线程池(ThreadPoolExecutor)使用学习

线程池现在是面试比较基础的知识点了,很多人可能都会去学一下线程池的几个参数,但是对真实使用过程中的问题却思考的不多。这里我们来仔细分析一下线程池的一些问题。

为什么要用线程池

我们当然可以手动创建,像下面这样:

    public void threadCreateTest() {
        for (int i = 0; i < 1000; i++) {
            final int x = i;
            new Thread(() -> {
                System.out.println("thread-" + x);
            }).start();
        }
    }

也可以用线程池,

    public void threadPoolTest() {
        ThreadPoolExecutor threadPoolExecutor = new
                ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 1000; i++) {
            final int x = i;
            threadPoolExecutor.submit(() -> System.out.println("thread-" + x));
        }
    }

但是,对于上面两个功能大致相同的方法,其中一个要创建1000个线程,另外一个只需要创建5个即可。同一个电脑上统计这两个的执行时间第一个time: 166ms,第二个time: 91ms,可见,并不是说创建的线程越多,执行速度就越快(线程的创建过程也会消耗系统资源)。

怎么用

创建线程池

可以使用Executors来创建线程池,些方法创建出来的线程池主要有四种:

  • newFixedThreadPool

    固定线程数量的线程池,BlockingQueue使用的是LinkedBlockingQueue(无长度限制)

  • newSingleThreadExecutor

    创建一个只有一个worker线程的线程池,BlockingQueue使用的是LinkedBlockingQueue

  • newCachedThreadPool

    创建一个按需创建新线程的线程池,会复用之前创建的线程池。此线程池的BlockingQueue使用的是非公平的SynchronousQueue

  • newScheduledThreadPool

    使用的BlockingQueueDelayedWorkQueue

  • newWorkStealingPool

    1.8新增加的,不是ThreadPoolExecutor,而是ForkJoinPool,会创建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。

上面是使用JDK提供的线程池工厂来创建线程池,也可以自己使用ThreadPoolExecutor的构造函数去创建线程池。

image-20220308103339053

线程池参数

其实ThreadPoolExecutor里面的注释已经讲的很清楚了,构造函数一共有7个

  • corePoolSize(核心线程数),在一般情况下,当执行任务的线程数量达到核心线程数之后,即便这里面的某些线程是空闲的,也仍会存在,除非设置了allowCoreThreadTimeOut参数

  • maximumPoolSize(最大线程数),当前正在执行任务的线程数量等于核心线程数,且队列已满的情况下才会继续创建新的线程来执行任务,直到线程数量等于最大线程数。所以如果使用无界阻塞队列(LinkedBlockingQueue),则maximumPoolSize不会起作用,而如果最大线程数=核心线程数,这就是一个固定线程数的线程池。

  • keepAliveTime(空闲线程存活时间),当前执行任务的线程数大于核心线程数,且部分线程处于空闲状态,则会被回收,此参数结合TimeUnit即为空闲多久之后会被回收

  • unit(TimeUnit 时间单位,会转换为纳秒),结合keepAliveTime使用,用来明确空闲的时间。

  • workQueue(任务队列)放置需要执行的任务的队列,必需是BlockingQueue(阻塞队列)

  • threadFactory(线程工厂)创建线程的工厂,可以指定线程分组、名称等,如果使用默认的线程工厂,此线程池里面的线程都属于一个group。

  • handler(任务拒绝策略RejectedExecutionHandler),即新submit的任务没有资源执行,线程数量已经达到最大线程数,且任务队列已满,提供了四种拒绝策略,默认使用的是AbortPolicy

    • AbortPolicy

      拒绝提交的任务,抛出RejectedExecutionException异常。

    • CallerRunsPolicy

      当前提交任务的线程执行任务。

    • DiscardOldestPolicy

      取消当前未执行且等待(排队)时间最久的任务(在BlockingQueue中),再交给线程池执行。

    • DiscardPolicy

      只是取消要提交的任务,不抛出异常。

其他方法

多数时候,我们使用线程池只是直接submit,或者有些场景需要等待任务很行完,拿到任务执行的结果,加上CountDownLatch等。

而ThreadPoolExecutor还给我们提供了其他的方法(有很多,get类型的我们略过),只看部分方法。

  • beforeExecute 执行任务前,也是模板方法的一种使用,可以用来重新初始化(re-intialize)ThreadLocals,或者记录日志等

  • afterExecute 执行任务后

  • finalize 执行shutdown

  • isShutdown 判断是不是shutdown isTerminated 所有任务在shutdown后执行完成,返回true,如果没有先执行过shutdown或者shutdownNow,则肯定不为true isTerminating 即正在终止中,当调用了shutdown或者shutdownNow后,并没有完成terminated,返回true。

  • prestartAllCoreThreads 启动所有核心线程,使它们空闲等待工作,会覆盖仅在执行新任务时启动核心线程的默认策略,返回启动的线程数

  • prestartCoreThread 启动一个核心线程,使其空闲等待工作,会覆盖执行新任务时启动核心线程的默认策略,如果所有核心线程都已启动,此方法将返回 false

  • purge 尝试从工作队列中删除所有已取消的 Future 任务

  • remove 从任务队列中移除任务

  • shutdown 有序关闭,执行先前提交的任务,但不会接受新任务

  • **shutdownNow ** 尝试停止所有正在执行的任务,从queue中把正在等待执行的任务移除

  • terminated 一个用来扩展的方法,停止线程池,默认空实现

线程池参数设置

创建线程池时,我们需要思考如何设置核心线程数与最大线程数

一般来说,需要考虑我们运行的任务是CPU密集型和IO密集型

IO密集型可以多设置一些,如2N(N为CPU核数)或者更多,比如Tomcat设置的默认线程数是100

CPU密集型设置的是N+1(N为CPU核数)。

但是如果任务即有IO又有CPU的情况该如何设置,其实我们需要考虑的是让程序能在有IO的情况下,还能充分利用CPU,如果IO比较多(即多数时候线程等待时间长),那么就需要更多的线程,反之,则需要较少的线程。

通过查阅资料,发现多数情况下可以参考利特尔法则

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

且需要根据实际的测试情况调用。

参考资料:

如何使用利特尔法则调整线程池大小

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章