Java线程池浅析

学习Java必定绕不开并发相关的主题,而线程相关的技术则是Java并发编程的核心之一。Java原生支持了线程(Thread),使用起来也非常简单,在Java中通过 new Thread() 即可在当前主线程下创建一个子线程,JVM会在调用该实例的start()方法后对应一个操作系统级别的线程,这样就完成了一个线程的创建。但是在大厂的开发规范例如著名的<<阿里巴巴Java开发手册>>中就明确提到了禁止显示的创建线程,也就是上述的使用 new Thread() 的方式,而是推荐使用线程池的方式创建线程。

为什么要使用线程池

在说使用线程池的好处之前可以先说明一下直接创建线程的坏处是什么,主要有以下几点:

  1. 线程的创建和销毁开销非常大,每次使用都重新创建线程会增加系统负担,增大响应时间;
  2. 手动创建线程不方便进行管理,如果没有对线程数量进行控制会导致创建大量线程消耗系统内存等资源(每个线程需要独立分配栈空间,默认是1M,具体大小取决于JDK版本和OS),另外线程过多也会导致频繁切换线程浪费CPU资源;
  3. 线程创建在不同平台上并不统一,如不同平台和不同OS下创建线程的最大限制不同。

使用线程池则在一定程度上可以避免上述问题,带来一系列的好处:

  1. 降低资源消耗:通过池化技术复用线程,降低线程创建和销毁的开销;
  2. 提高响应速度:减小获得线程的时间,提高响应速度;
  3. 提高线程的可管理性:基于线程池可以对线程进行统一的分配、调优和监控;
  4. 提供额外的功扩展能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

Java的Executor框架

Java中线程池实现是JUC中的Executor框架,其核心实现类是ThreadPoolExecutor。

参数解析

ThreadPoolExecutor构造函数比较复杂,完整的参数有7个,具体如下

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  1. corePoolSize, 核心线程数,当提交任务到线程池如果当前线程数小于核心线程数,会直接创建一个新的线程用于执行该任务;
  2. maximumPoolSize,最大线程数,指线程池所能创建的最大线程数;
  3. keepAliveTime,线程空闲后存活时间,超时后线程会被销毁,只有线程池中线程数量大于corePoolSize或设置了allowCoreThreadTimeOut()(允许空闲核心线程超时)才会生效;
  4. unit,线程存活时间单位;
  5. workQueue,任务队列,用于保存等待执行的任务的阻塞队列;
  6. threadFactory,线程工厂,用于创建线程并且可以为线程设置相关的属性,例如设置线程名称,是否是守护线程;
  7. handler,拒绝策略,在线程池超过最大线程数并且任务队列也满了则会执行拒绝策略,Java默认提供了4种拒绝策略,分别是
    1. AbortPolicy:直接抛出RejectedExecutionException异常,这也是线程池默认的策略;
    2. CallerRunsPolicy: 使用调用者所在的线程来执行该任务,一般是主线程,需要注意的是该策略会打乱任务投递的执行顺序(该任务会先于队列中其他任务执行);
    3. DiscardOldestPolicy: 丢弃掉任务队列中最先进入(也就是原本要执行的下一个任务)的任务,并执行当前任务;
    4. DiscardPolicy:不进行处理直接丢弃当前任务。
      由此可见,这四种拒绝策略都存在一些问题,因此在实际使用中我们可能需要自己实现RejectedExecutionHandler。

使用方法

ThreadPoolExecutor使用也非常简单。

  1. 提交任务
    向线程池提交任务主要有两种方式,使用execute(Runnable runnable)用于提交不需要返回值的任务,使用submit(Callable callable)用于提交需要返回值的任务,其返回值是一个Future对象。
  2. 关闭线程池
    关闭线程池也有两种方式,shutdown()会阻止新任务提交到线程池,并且会在已提交的任务执行完后关闭,shutdownNow()不仅会阻止新任务提交,还会“粗暴”的关闭线程池内正在执行的任务,并且会移除任务队列中的任务。

线程池工作流程

说完了线程池的基本参数和简单使用我们看一下线程池的工作流程。

  1. 前corePoolSize个任务时,来一个任务就创建一个新线程;
  2. 后面再来任务,就把任务添加到任务队列里让所有的线程中;
  3. 如果队列满了就创建临时线程。如果总线程数达到maximumPoolSize,执行拒绝策略。
    具体流程可以参考下面的流程图:
    1111

实践中遇到的一些问题

  1. 线程池和ThreadLocal,在线程池和ThreadLocal配合使用时需要注意的线程池中的线程是会被复用的,所以ThreadLocal中的值会出现不符合预期的情况。
  2. Executors,虽然juc很热心的提供了Executors这个工厂类,但是各个Java开发手册并不推荐使用它,例如Executors中的newFixedThreadPool(int n)和newSingleThreadExecutor()均使用了无界队列(BlockingQueue的大小为Integer.MAX_SIZE),在极端情况下如果负载过大,会在工作队列中堆积大量的任务导致OOM;而newCachedThreadPool()所设置的最大线程数上限为Integer.MAX_SIZE,如果负载过大会导致线程池一直创建新的线程,最终会因为线程句柄耗尽,JVM抛出异常。

参考资料:

  1. Java线程池实现原理及其在美团业务中的实践
  2. Java并发编程实战
  3. Java并发编程的艺术
posted @ 2020-06-20 00:50  寒来袖间  阅读(131)  评论(0编辑  收藏  举报