Exetcutor线程池的基本使用与原理

176 阅读6分钟

Exetcutor线程池的基本使用与原理

前言

最近学习了Exetcutor线程池的基本原理,写篇文章总结归纳一下。

线程池是什么

线程池,顾名思义,就是线程缓存,线程在操作系统中是稀缺资源,如果无限地去创建线程不仅会消耗系统资源,而且会降低系统的稳定性。因此Java为我们提供了线程池来对线程进行统一分配、调度与监控。

为什么要使用线程池

试想一下在web系统的场景,服务器通常需要接受并处理请求,所以要为每一个请求开启一个线程来进行处理。这样看起来好像也没啥毛病,只不过可能存在一个问题:

  • 如果并发的请求比较多,但每个线程执行的时间很短,这样就会频繁地去创建和销毁线程,我们知道CPU的运行速度是很快的,假设我们每个请求实际上要执行的时间很短,是不是就有可能出现我们系统花在创建和销毁线程的时间比处理请求的时间还要多

  • 这时候我们就会想到,那有没有办法让那么几个线程常驻在系统中,当来了一个任务的时候就可以直接执行,而不需要花时间去创建线程,执行完任务之后也不销毁,等待下一个任务。

  • 很显然,线程池就是干这个活儿的。线程池为线程生命周期的开销和资源不足的问题提供了解决方法。通过线程的重用,讲线程创建的开销分配给了多个任务。

  • 通过上面的描述,我们可以大概总结一下,什么时候使用线程池:

    • 单个任务处理时间比较短
    • 需要处理的任务数量很大

从线程的创建方式开始说起

  • Java为我们提供Thread类来,我们只需要继承Thread类,然后把自己的业务逻辑重写到run方法中,调用Thread.start()方法就可以创建一个线程并执行run方法了。
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }

    static class MyThread extends Thread{
        public void run(){
            System.out.println("创建一个线程。");
        }
    }
}
  • 基于Runnable接口实现,Runnable的含义表示的就是一个任务,实现了Runnable接口的人都会被Thread执行,例如:
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }

    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("执行Runnable任务");
        }
    }
}
  • 基于Callable接口实现,Callable同样是表示一个任务,与Runnable的区别在于它可以接收泛型,并且可以有返回值,用法也很简单,实现call()方法.
public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> future = new FutureTask<>(myCallable);
        Thread thread = new Thread(future);
        thread.start();
        //任务的返回值获取
        String s = future.get();
        System.out.println(s);
    }

    static class MyCallable implements Callable<String>{

        @Override
        public String call() throws Exception {
            return "执行callable任务";
        }
    }
}
  • 可以看到通过new一个Thread可以把我们不同的线程任务来执行,这就是每来一个任务就创建一个线程,任务执行结束或者报错的话,线程会自动销毁。我们上面说了,这种方式太消耗资源,接下来该轮到线程池出场了。

Executor框架

Executor接口是线程池框架中最基础的部分,定义了一个用于执行Runnable任务的execute方法。其主要的实现类图如下所示:

image.png

从图中可以看出Executor下有一个重要子接口ExecutorService,其中定义了线程池的具体行为

  • execute(Runnable command):执行Runnale类型的任务,无返回值

  • submit(task):可用来提交Callable或Runnable任务,有返回值,返回Futrue类型的对象

  • shutdown():停止接受新任务,完成当前存在的任务后关闭

  • shutdownNow():立刻停止当前所有任务并关闭

  • isTerminated():判断是否所有任务都执行完毕了

  • isShutdown():判断该线程池是否关闭

线程池的具体实现:ThreadPoolExecutor

  • 先看ThreadPoolExecutor的简单使用
public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
        for(int i=0; i < 100; i++){
            //任务提交,无返回值
            threadPoolExecutor.execute(new MyRunnable());
            //任务提交,有返回值,用于传callable
            //threadPoolExecutor.submit(new MyCallable());
        }
        //执行完任务后关闭
        threadPoolExecutor.shutdown();
    }

    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("执行Runnable任务");
        }
    }
}
  • 构造方法
public ThreadPoolExecutor(int corePoolSize, 
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
  • 参数说明:
    • corePoolSize:核心线程数,最小值是0,最大值是Integer.MAX_VALUE

    • maximumPoolSize: 最大线程数,最小值是0,最大值是Integer.MAX_VALUE

    • keepAliveTime:线程允许的空闲时间,最小值是0,最大值是Integer.MAX_VALUE

    • unit:keepAliveTime的单位

    • workQueue: 待执行任务的阻塞队列,任务必须实现Runnable接口或者Callable接口,JDK提供了以下四种:

      1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;

      2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;

      3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;

      4、priorityBlockingQuene:具有优先级的无界阻塞队列;

    • handler:拒绝策略,当阻塞队列满了之后,并且没有空闲的工作线程,对后面提交的任务要采取的拒绝策略,内部类有以下4种,可自己实现RejectedExecutionHandler接口自定义拒绝策略

      1、AbortPolicy:直接抛出异常,默认策略;

      2、CallerRunsPolicy:用调用者所在的线程来执行任务;

      3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

      4、DiscardPolicy:直接丢弃任务;

线程池的工作原理

  • 线程池工作原理图示

image.png

  • 线程池工作原理描述:以下面线程池的创建参数结合原理图进行说明(
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 8, 30, TimeUnit.SECONDS,
        new LinkedBlockingDeque<>(100), new ThreadPoolExecutor.CallerRunsPolicy());
  1. 线程池接受到来自execute/submit方法提交过来的任务时,先看corePool中有没有空闲的线程,如果有核心线程空闲,则当前任务交给其中一个空闲的核心线程处理进行处理。(如果当前的核心线程数还没到达corePool的值,不管有没有空闲的核心线程都会创建一个新的线程来执行任务)如上例所示,核心线程数为4,则前4个任务会优先交给核心线程处理。核心线程是常驻线程,不受配置keepAliveTime的控制,也就是说核心线程不会被线程池回收。
  2. 当corePool中的线程都被任务占满了之后,后来的任务会放到阻塞队列BlockingQueue中暂存,等有核心线程空闲之后,再从BlockingQueue的队头中逐个取下任务来执行。
  3. 如果阻塞队列也放满了,那么就会往maximumPool中创建线程(非核心线程)来接受任务,如上例中,maximumPoolSize=8,则还可以创建maximumPoolSize-corePoolSize=4个线程来接受任务。非核心线程在执行完任务之后会处于空闲状态,如果在设置的线程允许的空闲时间内不干活,则会被线程池回收,等下次需要的时候再进行创建。
  4. 如果非核心线程创建的数量已经达到了极限,如本例中,线程池的线程数已经达到了maximumPoolSize=8,即线程池没有空闲线程,且阻塞队列也放不下了,那么线程池就会对这之后过来的任务进行拒绝,被拒绝的任务会根据设置的拒绝策略进行执行。本例中设置的拒绝策略是CallerRunsPolicy,则该任务会交回给主线程进行执行。
  • exucute方法的流程图如下:

image.png

线程池的监控

ThreadPoolExecutor线程池提供了以下几个实例方法来对当前线程池实例进行监控

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS,
        new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
//获取当前线程已执行和未执行的任务总数
threadPoolExecutor.getTaskCount();
//获取已执行完成的任务数
threadPoolExecutor.getCompletedTaskCount();
//获取线程池当前的线程数
threadPoolExecutor.getPoolSize();
//获取线程池当前的活跃线程数
threadPoolExecutor.getActiveCount();

线程池最大线程数的配置建议值

  • CPU密集型:线程数 = CPU核心数+1
  • I/O密集型:线程数 = 2 * CPU核心数
  • 核心线程数不宜设置过大,因为受CPU核数的限制,核心线程接受了任务,但线程抢占不到CPU依然不会立刻执行。具体性能最佳参数的配置应该通过压测结果来进行多次调整得出。

线程池的使用规范

摘选阿里巴巴开发手册编码规约的程池使用部分与大家共勉

image.png

结语

本文重点阐述ThreadPoolExecutor线程池的原理和任务执行的流程,对于线程和线程池的概念等内容没有深入去描述。如有不足之处,敬请指正。