写点什么

一文全貌了解线程池的正确使用姿势

作者:JAVA旭阳
  • 2022-10-22
    福建
  • 本文字数:5467 字

    阅读完需:约 18 分钟

概述

线程池在平时的工作中出场率非常高,基本大家多多少少都要了解过,可能不是很全面,本文和大家基于 jdk8 学习下线程池的全面使用,以及分享下使用过程中遇到的一些坑。

线程池介绍

因为线程资源十分宝贵,每次创建和销毁线程的开销都比较大,另一方面,如果创建太多的线程,也会消耗系统大量资源,降低系统吞吐量,甚至导致服务不可用。为了解决这些问题,提出一种基于池化思想管理和使用线程的机制,就是我们的线程池。


线程池的核心思想就是能做到线程的复用,线程池中的线程执行完成不会销毁,而是存留在内存里,等待执行其他的任务。


jdk 中的线程池采用的是一种生产者—消费者模型,如下图:



  • 外部提交任务到线程池中,如果线程数量小于指定阈值的话,直接创建线程

  • 如果提交任务大于阈值,会存到队列中

  • 线程池中的工作线程执行前面的任务完成后,不会销毁,而是去从队列中获取任务,继续执行。

线程池创建

线程池提供如下 2 种方式创建方式:


ThreadPoolExecutor 创建

下面是线程池类 ThreadPoolExecutor 最全参数的构造函数


ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler)
复制代码


  1. corePoolSize: 核心线程数,线程池中始终存活的线程数量

  2. maximumPoolSize:最大线程数,线程池中容纳最大的线程数量,这里引入一个"救急线程"的概念,可以想象为"临时工",它的数量=maximumPoolSize-corePoolSize,这部分线程会超过一定时间后销毁。

  3. keepAliveTime:"救急线程"的可以存活的时间,当超过这段时间这些线程没有任务执行,就会被回收。

  4. unit:单位,和 keepAliveTime 配合使用。

  5. workQueue: 阻塞队列,用来存储提交的多余的任务,等待工作线程执行完毕后获取,它有下面 7 个类型:



  1. threadFactory: 线程工厂,用于创建线程,可以指定线程名。

  2. handler: 拒绝策略,如果任务超限时执行的策略,内置了 4 种可选,默认AbortPolicy,也可以自定义。



通过这些参数创建好线程后,提交一个线程的执行流程图如下:



  • 当线程数小于核心线程数时,创建线程。

  • 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。

  • 当线程数大于等于核心线程数,且任务队列已满, 若线程数小于最大线程数,创建救急线程,否则执行拒绝策略。

Executors 创建

由于上面线程池的构造方法比较复杂,jdk 也为我们提供了一种便利的方式,通过 Executors 工厂创建多种不同的线程池。

newFixedThreadPool

创建一个固定大小的线程池


 public static ExecutorService newFixedThreadPool(int nThreads) {        return new ThreadPoolExecutor(nThreads, nThreads,                                      0L, TimeUnit.MILLISECONDS,                                      new LinkedBlockingQueue<Runnable>());    }
复制代码


  • 核心线程数等于最大线程数

  • 阻塞队列是无界的,可以不限制任务数量,可能会因为任务太多 OOM

  • 使用默认的线程工厂和拒绝策略

  • 适用于任务量已知、相对耗时的任务

newCachedThreadPool

创建一个核心线程为 0,最大线程数不限的线程池


public static ExecutorService newCachedThreadPool() {        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                      60L, TimeUnit.SECONDS,                                      new SynchronousQueue<Runnable>());}
复制代码


  • 核心线程数为 0,最大线程数不限制,来一个任务就会创建一个线程,过一段时间会销毁,这样可能会导致线程过多而导致系统资源耗尽。

  • 队列采用了 SynchronousQueue 实现,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)。

  • 适合任务数比较密集,但每个任务执行时间较短的情况。

newSingleThreadExecutor

创建只有一个线程的线程池。


public static ExecutorService newSingleThreadExecutor() {        return new FinalizableDelegatedExecutorService            (new ThreadPoolExecutor(1, 1,                                    0L, TimeUnit.MILLISECONDS,                                    new LinkedBlockingQueue<Runnable>()));    }
复制代码


  • 核心线程数和最大线程数都为 1,任务数多于 1 时,会放入无界队列排队。

  • 适用于只有一个任务执行情况。


问题: newSingleThreadExecutor 和 newFixedThreadPool(1)区别是什么呢?


newSingleThreadExecutor 中创建的线程通过 FinalizableDelegatedExecutorService 实现,采用装饰器模式,只对外暴露了 ExecutorService 接口,后续也无法修改线程池的大小。而 Executors.newFixedThreadPool(1) 初始时为 1,以后还可以修改,对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改。

newScheduledThreadPool

创建可以执行延迟任务的线程池


public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {        return new ScheduledThreadPoolExecutor(corePoolSize);    }
复制代码


  • 适用于一些延迟执行的调度任务

newWorkStealingPool

这是 jdk8 引入的一种方式,创建一个抢占式执行的线程池(任务执行顺序不确定)。


public static ExecutorService newWorkStealingPool() {        return new ForkJoinPool            (Runtime.getRuntime().availableProcessors(),             ForkJoinPool.defaultForkJoinWorkerThreadFactory,             null, true);    }
复制代码


  • 注意这个不是基于 ThreadPoolExecutor 创建出来,而是基于 ForkJoinPool 扩展,将任务按照工作线程均分。然后先工作完的线程去帮助没处理完的线程工作。以实现最快完成工作。

  • 适合处理很耗的任务。

线程池关键 API 和例子

提交执行任务 API

  • void execute(Runnable command)


提交执行 Runnable 任务,无返回值


  • Future<T> submit(Callable<T> task)


提交任务 callable 任务,用返回值 Future 获得任务执行结果,主线程可以执行 FutureTask.get()方法来阻塞等待任务执行完成。


  • Future<?> submit(Runnable task)


提交 Runnable 任务,用返回值 Future 获得任务执行结果,主线程可以执行 FutureTask.get()方法来阻塞等待任务执行完成。


  • Future<T> submit(Runnable task, T result)


提交 Runnable 任务,用返回值 Future 获得任务执行结果,返回传入的 result, 主线程可以执行 FutureTask.get()方法来阻塞等待任务执行完成。


  • List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)


批量提交 Callable 任务,用返回值 Future 获得任务执行结果,主线程阻塞等待任务执行完成。


  • List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)


带超时时间的批量提交 Callable 任务,用返回值 Future 获得任务执行结果,主线程阻塞等待任务执行完成或者过了超时时间。


  • T invokeAny(Collection<? extends Callable<T>> tasks)


提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消


  • T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)


提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间


@Test    public void test1() throws ExecutionException, InterruptedException {        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 100,                100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(50),                new ThreadPoolExecutor.DiscardPolicy());        for (int i = 0; i < 20; i++) {            // execute的方式提交任务            threadPool.execute(() -> {                log.info("execute ....");            });        }
// submit runnable Future<String> futureCall = threadPool.submit(new Callable<String>() { @Override public String call() throws Exception { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return "callable Result"; } }); // 阻塞等待结果返回 String result = futureCall.get(); log.info("submit callable: {}", result);
// submit runnable Future<String> future = threadPool.submit(new Runnable() { @Override public void run() { log.info("submit runnable ...."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "submit result"); // 阻塞等待结果返回 result = future.get(); log.info("submit runnable: {}", result);
List<Callable<String>> callables = new ArrayList<>(); for (int i = 0; i < 5; i++) { final int j = i; callables.add(new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(2000); return "callable" + j; } }); } List<Future<String>> futures = threadPool.invokeAll(callables); for (Future<String> stringFuture : futures) { String invoke = stringFuture.get(); log.info("invoke result: {}", invoke); }
String invokeAny = threadPool.invokeAny(callables); log.info("invoke any: {}", invokeAny); }
复制代码

关闭线程池 API

  • shutdown()


优雅关闭线程池,不会接收新任务,但已提交任务会执行完,包括等待队列里面的。


  • List<Runnable> shutdownNow()


立即关闭线程池,不会接收新任务,也不会执行队列中的任务,并用 interrupt 的方式中断正在执行的任务,返回队列中的任务。


  • isShutdown()


返回线程池是否关闭


  • isTerminated()


如果在关闭后所有任务都已完成,则返回 true。注意,除非先调用 shutdown 或 shutdownNow,否则 istterminated 永远不会为 true。


  • boolean awaitTermination(long timeout, TimeUnit unit)


阻塞直到所有任务在关闭请求后完成执行,或发生超时,或当前线程被中断(以先发生的情况为准)。如果该执行程序终止,则为 True;如果在终止前超时,则为 false。

线程池监控 API

  • long getTaskCount():获取已经执行或正在执行的任务数

  • long getCompletedTaskCount(): 获取已经执行的任务数

  • int getLargestPoolSize():获取线程池曾经创建过的最大线程数,根据这个参数,我们可以知道线程池是否满过

  • int getPoolSize(): 获取线程池线程数

  • int getActiveCount(): 获取活跃线程数(正在执行任务的线程数)

扩展 API

ThreadPoolExecutor留下了 3 个扩展接口供我们使用。


  • protected void beforeExecute(Thread t, Runnable r) : 任务执行前被调用

  • protected void afterExecute(Runnable r, Throwable t): 任务执行后被调用

  • protected void terminated() : 线程池结束后被调用


@Test    public void test3() {        ExecutorService executor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1)) {            @Override protected void beforeExecute(Thread t, Runnable r) {                System.out.println("beforeExecute is called");            }            @Override protected void afterExecute(Runnable r, Throwable t) {                System.out.println("afterExecute is called");            }            @Override protected void terminated() {                System.out.println("terminated is called");            }        };
executor.submit(() -> System.out.println("this is a task")); executor.shutdown(); }
复制代码


运行结果:


使用注意事项

  1. 创建线程池的时候,根据阿里巴巴规范,创建线程池的时候根据使用场景自定义ThreadPoolExecutor的方式,尽量避免是使用Executors

  2. 只有当任务都是同类型并且互相独立,线程池的性能才能达到最佳。


  • 如果将运行时间较长的与运行时间较短的任务混合在一起,可能造成"拥塞"。

  • 如果提交的任务依赖于其他任务,比如某任务等待另一任务的返回值或执行结果,而这他们是提交到同一个 Executor 中,这种情况就会发生线程饥饿锁。


  1. 在线程池中会导致从 ThreadLocal 中获取数据发生混乱,应该尽量避免使用。

  2. 如果使用 submit 提交任务,会吞掉异常日志,在线程池中尽量使用 try catch 捕获异常。

参考

https://juejin.cn/post/6844903889678893063#heading-45


https://www.cnblogs.com/vipstone/p/14149065.html


https://segmentfault.com/a/1190000038220635


https://www.cnblogs.com/pcheng/p/13540619.html


https://www.jianshu.com/p/7ab4ae9443b9


https://juejin.cn/post/6986863481764970510#heading-14

发布于: 刚刚阅读数: 3
用户头像

JAVA旭阳

关注

还未添加个人签名 2018-07-18 加入

还未添加个人简介

评论

发布
暂无评论
一文全貌了解线程池的正确使用姿势_Java_JAVA旭阳_InfoQ写作社区