Java多线程(十二) 类比理解线程池 && ThreadPoolExecutor

Java多线程(十二) 类比理解线程池 && ThreadPoolExecutor

线程池

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处:

  1. 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性

线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

举个生活中的例子

直接讲解线程池会比较抽象,在这里我先讲一个生活中的例子——银行柜台的例子,把这个生活中的例子看明白了,下面类比讲解线程池就非常容易理解了,在这里要首先感谢B站up主“遇见狂神说”提供的这个生动的例子。

银行取钱的时候会设有窗口,例如下面窗口一共设有5个窗口,但是正常情况下一直开放的窗口是2个(窗口1和窗口2),此外,还设有等候区,等候区有5个座位,可以令5个人等候。

但是窗口3到窗口5不可能一直关闭,当窗口1、窗口2被占用,并且等候区的5个座位也都被占用时,就需要打开一个关闭着的窗口让其参与工作。如果人慢慢少了以后,发现一段时间(例如1小时)里前两个窗口足够处理业务了,没有人去窗口3到窗口5,那么就把窗口3到窗口5再关闭。

如果有一天人特别多,所有窗口都已经打开了,并且等候区所有位置都被占用,这个时候如果还有人想进来,就需要调用拒绝策略了。

理解线程池

线程池中有下面几个模块,理解了上面的例子,通过类比的方法很容易可以理解他们:

  1. 线程池的线程数 maximumPoolSize :线程池可以提供的线程数量,类比于上个例子中的五个窗口
  2. 核心线程池数 corePoolSize :线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁不会被关闭,类比于上个例子中的窗口1、窗口2
  3. 阻塞队列 workQueue:线程池中用来保存没有线程接收的提交的新任务,类比于等候区
  4. 线程活动保持时间 keepAliveTime:一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,类比于上面提到的一小时。
  5. 拒绝策略:当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时有新任务提交进来就需要执行拒绝策略。具体的拒绝策略在后文后提到。

在这里插入图片描述

上面这幅图讲解的是Java线程池执行execute后的操作:

  1. 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)
  2. 如果运行的线程等于或多于corePoolsize,则将任务加入BlockingQueue
  3. 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)
  4. 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用拒绝策略

不知道读者有没有发现很奇怪的问题,就是为什么有了新任务先让他进入阻塞队列而不是直接新增加线程呢(也就是为什么先执行步骤2而不是先执行3创新新线程)?
Java多线程采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在线程池完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

拒绝策略

JUC默认提供了四种拒绝策略:

  1. CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。例如主线程创建的线程池执行拒绝策略,那么由主线程负责执行任务

  2. AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。

  3. DiscardPolicy - 直接丢弃,其他啥都没有,不会抛出异常

  4. DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入,不会抛出异常

线程池的使用

创建线程池

可以通过ThreadPoolExecutor来创建一个线程池,ThreadPoolExecutor有七个参数:

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

上面的6个参数在上面已经介绍了,这里说一下ThreadFactory 线程工厂这个参数,他用来设置创建线程的工厂。

向线程池提交任务

可以使用execute() 和 submit() 两个方法向线程池提交任务。

execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功,execute() 方法传入的是一个Runnable类的实例。

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过,这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

关闭线程池

可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

优化线程池

线程池中有一个参数是最大线程数,那么这个最大线程数该如何设定呢:

  1. 对于CPU密集型的任务,应该配置尽可能小的线程数,可以设置成CPU数+1,可以通过指令Runtime.getRuntime().availableProcessors()来获取当前电脑的CPU数
  2. 对于IO密集型的任务,应该设置尽可能多的线程,比如设置成CPU数*2

线程池例子

需要线程数 <= 核心线程数+阻塞队列大小

public class MyPool {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                60, 
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try{
            for (int i = 0; i < 5; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"execute");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

在这里插入图片描述
可以看到调用的线程数只有核心线程池数,因为阻塞队列有足够的地方存储未分配的任务。

核心线程数+阻塞队列大小 < 需要线程数 <=线程池最大线程数 + 阻塞队列大小

public class MyPool {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                60, 
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try{
            for (int i = 0; i < 10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"execute");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

在这里插入图片描述
因为阻塞队列已经不够用了,因此会打开更多的线程接受任务。

线程池最大线程数 + 阻塞队列大小 < 需要线程数

public class MyPool {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                60, 
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try{
            for (int i = 0; i < 15; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"execute");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

在这里插入图片描述
因为任务数量大于线程池最大线程数 + 阻塞队列大小,因此会调用拒绝策略,我使用的是拒绝策略是会报错的AbortPolicy,因此会输出异常。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值