java 线程池

ThreadPoolExecutor与ForkJoinPool区别在于前者每个线程共享队列,后者每个线程有各自的队列

 

一 入参

corePoolSize: 线程池核心线程数,当初始化线程池时,会创建核心线程进入等待状态,即使它是空闲的,核心线程也不会被摧毁,从而降低了任务一来时要创建新线程的时间和性能开销。当allowCoreThreadTimeOut手动设置为true,才会销毁

maximumPoolSize: 最大线程数,意味着核心线程数都被用完了,那只能重新创建新的线程来执行任务,但是前提是不能超过最大线程数量,否则该任务只能进入阻塞队列进行排队等候,直到有线程空闲了,才能继续执行任务

keepAliveTime: 线程存活时间,除了核心线程外,那些被新创建出来的线程可以存活多久。意味着,这些新的线程一但完成任务,而后面都是空闲状态时,就会在一定时间后被摧毁

threadFactory:就是创建线程的线程工厂

unit: 线程存活时间单位
workQueue: 表示任务的阻塞队列,由于任务可能会有很多,而线程就那么几个,所以那么还未被执行的任务就进入队列中排队,队列我们知道是 FIFO 的,等到线程空闲了,就以这种方式取出任务。这个一般不需要我们去实现。handler的拒绝策略有四种:
  第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
  第二种DisCardPolicy:不执行新任务,也不抛出异常
  第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
  第四种CallerRunsPolicy:直接调用execute来执行当前任务

1. LinkedBlockingQueue
  对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列。由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。
2. SynchronousQueue
  第二种阻塞队列是 SynchronousQueue,对应的线程池是 CachedThreadPool。线程池 CachedThreadPool 的最大线程数是 Integer 的最大值,可以理解为线程数是可以无限扩展的。CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。
我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。
3. DelayedWorkQueue
  第三种阻塞队列是DelayedWorkQueue,它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

 

二 线程池介绍

FixedThreadPool(建固定核心数的线程池):定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程,而且它们的线程数存活时间都是无限的,并发执行

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

对比 newSingleThreadPool,其实改变的也就是可以根据我们来自定义线程数的操作,比较相似。我们通过newFixedThreadPool(2)给它传入了 2 个核心线程数

WorkStealingPool(创建一个具有抢占式操作的线程池):JDK1.8 版本加入的一种线程池,stealing 翻译为抢断、窃取的意思,它实现的一个线程池和之前4种都不一样,用的是 ForkJoinPool 类,构造函数代码如下

    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

parallelism => Runtime.getRuntime().availableProcessors() - 这是JVM可用的处理器数。
handler => ForkJoinPool.defaultForkJoinWorkerThreadFactory - 返回新线程的默认线程工厂。
asyncMode => true – 使其在aysnc模式下工作,并为分叉的任务设置FIFO顺序,这些任务永远不会从其工作队列中加入
  最明显的用意就是它是一个并行的线程池,参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别,前面4种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。从介绍中,还说明这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作

SingleThreadPool(创建单核心的线程池):单核心线程池,最大线程也只有一个,这里的时间为 0 意味着无限的生命,就不会被摧毁了,适用于有顺序的任务的应用场景

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

 

CachedThreadPool(创建一个自动增长的线程池):可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,默认60秒,适用于耗时少,任务量大的情况

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

ScheduledThreadPool(创建一个按照计划规定执行的线程池):周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

内部有一个延时的阻塞队列来维护任务的进行,延时也就是在这里进行的。我们把创建 newScheduledThreadPool 的代码放出来,这样对比效果图的话,显得更加直观。

// 参数2:延时的时长
scheduledExecutorService.schedule(th_all_1, 3000, TimeUnit.MILLISECONDS);
scheduledExecutorService.schedule(th_all_2, 2000, TimeUnit.MILLISECONDS);
scheduledExecutorService.schedule(th_all_3, 1000, TimeUnit.MILLISECONDS);
scheduledExecutorService.schedule(th_all_4, 1500, TimeUnit.MILLISECONDS);
scheduledExecutorService.schedule(th_all_5, 500, TimeUnit.MILLISECONDS);

 

线程池的监控

通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
    getTaskCount:线程池已经执行的和未执行的任务总数;
    getCompletedTaskCount:线程池已完成的任务数量,该值小于等于taskCount;
    getLargestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize;
    getPoolSize:线程池当前的线程数量;
    getActiveCount:当前线程池中正在执行任务的线程数量。
通过这些方法,可以对线程池进行监控,在ThreadPoolExecutor类中提供了几个空方法,如beforeExecute方法,afterExecute方法和terminated方法,可以扩展这些方法在执行前或执行后增加一些新的操作,例如统计线程池的执行任务的时间等,可以继承自ThreadPoolExecutor来进行扩展。

 

四 线程池优化策略

CPU密集型
  说到优化,肯定离不开业务。比如我们的任务是计算密集型(以CPU计算为主)、多内存计算的,那么可能4c的机器开了4-8个线程,负载就打满了。那么我们在设置最大线程数maximumPoolSize的时候,最好使用Runtime.availableProcessors方法获取可用处理器的个数N,并设置maximumPoolSize=N+1。(额外+1的原因是当计算密集型线程偶尔由于页缺失故障或其他原因而暂停时,这个“额外的”线程也能确保这段时间内的CPU始终周期不会被浪费)
IO密集型
  像读写磁盘文件、读写数据库、网络请求等阻塞操作,执行IO操作时,CPU处于等待状态,等待过程中操作系统会把CPU时间片分给其他线程。我们可以使用newCachedThreadPool。

  newCachedThreadPool默认最大线程数为Integer.MAX_VALUE,keepAliveTime只有60s,队列也是无界队列。这种就适合用于一些生命周期较短,密集而又频繁的操作。
也可以参考newCachedThreadPool,根据实际情况(内存上线的控制很关键),适当缩小maximumPoolSize的大小,增加或减小keepAliveTime

CPU/IO混合型任务
  大多数任务并不是单一的计算型或IO型,而是IO伴随计算两者混合执行的任务——即使简单的Http请求也会有请求的构造过程。混合型任务要根据任务等待阻塞时间与CPU计算时间的比重来决定线程数量:

 

   比如一个任务包含一次数据库读写(0.1ms),并在内存中对读取的数据进行分组过滤等操作(5μs),那么线程数应该为80左右(假设为4c的机器)。阻塞的时间(waitTime)对计算的时间(computeTime)占比越大,则开放的线程数也应该越多

  线程池的大小取决于任务的类型以及系统的特性,避免“过大”和“过小”两种极端。线程池过大,大量的线程将在相对更少的CPU和有限的内存资源上竞争,这不仅影响并发性能,还会因过高的内存消耗导致OOM;线程池过小,将导致处理器得不到充分利用,降低吞吐率
  要想正确的设置线程池大小,需要了解部署的系统中有多少个CPU,多大的内存,提交的任务是计算密集型、IO密集型还是两者兼有
  虽然线程池和JDBC连接池的目的都是对稀缺资源的重复利用,但通常一个应用只需要一个JDBC连接池,而线程池通常不止一个。如果一个系统要执行不同类型的任务,并且它们的行为差异较大,那么应该考虑使用多个线程池,使每个线程池可以根据各自的任务类型以及工作负载来调整

 

五 线程池的五种状态

  线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

  线程池各个状态切换框架图:

  

 

 

 1.RUNNING

    状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理
    状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

2.SHUTDOWN
    状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务
    状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN

3.STOP
    状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务
    状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP

4.TIDYING
    状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现
    状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING
    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING

5.TERMINATED
    状态说明:线程池彻底终止,就变成TERMINATED状态
    状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED

FAQ:

1 线程池的核心线程会销毁吗?
  当allowCoreThreadTimeOut手动设置为true或者执行的run方法抛出异常,核心线程都会被销毁,但是后者还是会创建新的线程称呼来,前者则销毁什么都不做,关键在于allowCoreThreadTimeOut为true则下面代码直接返回,不在执行addWorker方法

2 线程池的执行流程?

 

 



posted on 2021-08-10 20:56  胡子就不刮  阅读(968)  评论(0编辑  收藏  举报

导航