理解线程池


前言

回想之前我们所学的数据库连接池,其基本原理是在内部对象池中维护一定数量的数据库连接,避免了频繁的建立、关闭连接,极大地提高了系统的性能。线程池的原理类似,在内部维护了一定数量的线程,可以重复使用,避免了频繁的创建和销毁线程造成的资源浪费以及大量的不必要的时间开销。本文主要介绍了ThreadPoolExecutor 类、核心参数、线程池的执行流程等内容。


一、ThreadPoolExecutor 类

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 —来自阿里开发规约
在这里插入图片描述
而且Executors的方法创建出来的常用线程池大多都调用了ThreadPoolExecutor类的构造方法去创建线程池。所以我们使用ThreadPoolExecutor来创建线程池,里面所有的参数我们都可以根据需求来自己指定,这样使用起来就放心很多。

在我们使用线程池之前,必须对ThreadPoolExecutor类有一个清晰的认识
ThreadPoolExecutor 继承了 AbstractExecutorService 类,是线程池中最核心的一个类
在这里插入图片描述
ThreadPoolExecutor提供了几个构造方法来初始化线程池,但其他的构造方法都最后都是调用下面这个七个参数的构造方法来实现的,所以我们直接学习最核心的部分。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

二、Java线程池核心参数

线程池的核心参数是经常会被问到的一点,也只有重复了解每个参数背后的含义,我们才可以写出较合理的线程池,关于几个参数的说明如下表所示。
在这里插入图片描述

结合代码,下面这段代码演示了创建一个由核心线程数量为2,线程池最大线程数量为5,非核心线程空闲200毫秒回收,长度为2的有界阻塞队列,默认的线程工厂,拒绝策略为CallerRunsPolicy七个参数组成的线程池
在这里插入图片描述
从核心数量为2,最大线程数量为5也可以得出非核心线程的最大数量为3

下面通过一个完整的案例演示线程池的执行效果。

public class Test {
    public static void main(String[] args) {
        //创建线程池                                 7
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
                5, 200,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        for (int i = 1; i <= 10; i++) {
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);//添加任务到线程池
            //执行任务除了可以使用 execute 方法还可以使用 submit 方法  
            //execute 适用于不需要关注返回值的场景,submit 方法适用于需要关注返 回值的场景
        }
        /* shutdownNow:停止当前正在执行的任务,取消未开始的任务,返回未开始执行的任务的列表*/
        /*shutdown:先把当前的任务执行完然后再去停止*/
        executor.shutdown();
    }
}
public class MyTask implements Runnable {

    private int taskNum;

    public MyTask(int num) {
        this.taskNum = num;
    }

    @Override
    public void run() {
        try {
            Thread.currentThread().sleep(3000); //休眠是会了更好的看到效果,现在cpu运行太快
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":task "+taskNum+"执行完毕");
    }
}

在这里插入图片描述
从上图我们可以看出10个任务被线程池执行完毕,且有些线程在这次运行中执行了多个任务,关于为什么任务8在main线程中执行的问题在下面的拒绝策略中会讲到。


三、执行流程

Java线程池的工作流程为:线程池刚被创建时,只是向系统申请了一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。
在这里插入图片描述
在调用execute()添加一个任务时,如果正在运行的线程数量少于核心线程数量,线程池会立刻创建线程并执行,如果大于就放入阻塞队列中,如果阻塞队列已满且正在运行的线程数量未达到最大值,线程池会创建非核心线程执行该任务,如果线程池的线程数已经达到最大值且阻塞队列也满,则线程池会采用拒绝策略进行处理。

当线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。

当非核心线程工作组中的线程处于空闲状态的时间超过keepAliveTime时间时,该线程会被认定为空闲线程并停止。因此,在所有线程任务都执行完毕后,线程池会收缩到核心线程数量的大小。


四、拒绝策略

如果线程池中的线程数量已满且阻塞队列也满,此时线程池的线程资源已耗尽。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。JDK内置的拒绝策略有四种,ThreadPoolExecutor中作为内部类提供。

4.1AbortPolicy 策略

 public static class AbortPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

直接抛出异常信息,阻止系统正常工作

4.2CallerRunsPolicy 策略

 public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

如果线程池未关闭且任务被拒绝了,则由提交任务的线程(例如:main)直接执行此任务

看到这里,关于上面那个例子为什么任务8会在main中执行的问题就可以迎刃而解了,因为线程池中最大线程数量为5,阻塞队列长度为2,最多只能存储7个任务,第8个任务被拒绝了但因为拒绝策略为CallerRunsPolicy,所以由提交任务的main线程执行。

4.3DiscardOldestPolicy策略

    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        public DiscardOldestPolicy() { }
        
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();//移除队列中最老的一个线程任务
                e.execute(r);//尝试提交当前任务
            }
        }
    }

移除线程队列中最老的一个线程任务,并尝试重新提交当前任务

4.4DiscardPolicy策略

    public static class DiscardPolicy implements RejectedExecutionHandler {
        public DiscardPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

直接丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下丢弃部分任务,这将是保障系统安全、稳定的一种很好的方案。


总结

线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。本文中介绍的关于线程池的内容都是非常重要的知识点,一定要熟练掌握。同时线程池还提供了自定义拒绝策略,用户可以自己扩展来实现。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JinziH Never Give Up

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值