J.U.C之深入理解Java线程池ThreadPoolExecutor

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

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。 当任务到达时, 任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。 线程是稀缺资源, 如果无限制地创建, 不仅会消耗系统资源,还会降低系统的稳定性, 使用线程池可以进行统一分配、 调优和监控。 但是, 要做到合理利用线程池, 必须对其实现原理了如指掌。

1、线程池工作流程

当向线程池提交一个任务之后, 线程池是如何处理这个任务的呢?下图展示了线程池的主要处理流程

ThreadPoolExecutor执行规则在这里插入图片描述

从图中可以看出, 当提交一个新任务到线程池时, 线程池的处理流程如下:

  1. 线程池判断核心线程池里的线程是否都在执行任务。 如果不是, 则创建一个新的工作线程来执行任务。 如果核心线程池里的线程都在执行任务, 则进入下个流程。
  2. 线程池判断工作队列是否已经满。 如果工作队列没有满, 则将新提交的任务存储在这个工作队列里。 如果工作队列满了, 则进入下个流程。
  3. 线程池判断线程池的线程是否都处于工作状态。 如果没有,则创建一个新的工作线程来执行任务。 如果已经满了,则交给饱和策略来处理这个任务。

2、线程池的使用(由于不推荐大家用Executors构造线程池, 所以这里只用标准构造方法)

在这里插入图片描述
  Java提供了自己的线程池。每次只执行指定数量的线程,java.util.concurrent.ThreadPoolExecutor 就是这样的线程池,我们可以通过ThreadPoolExecutor来创建一个线程池。创建代码如下:

ThreadPoolExecutor
  我们可以通过ThreadPoolExecutor来创建一个线程池。下面我们就来看一下ThreadPoolExecutor中的一个构造方法。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
创建一个线程池时需要输入几个参数,对参数的解释如下:
  1. corePoolSize(线程池的基本大小):当提交一个任务到线程池时, 线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程, 等到需要执行的任务数大于线程池基本大小时就不再创建。不超过maximumPoolSize值时,线程池中最多有corePoolSize 个线程工作。 如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

  2. runnableTaskQueue(任务队列): 用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:

    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列, 此队列按FIFO(先进先出) 原则对元素进行排序;

    • LinkedBlockingQueue: 一个基于链表结构的阻塞队列, 此队列按FIFO排序元素, 吞吐量通常要高于

    • ArrayBlockingQueue。 静态工厂方法Executors.newFixedThreadPool()使用了这个队列;

    • SynchronousQueue: 一个不存储元素的阻塞队列。 每个插入操作必须等到另一个线程调用移除操作, 否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列;

    • PriorityBlockingQueue: 一个具有优先级的无界阻塞队列。

    • 直接交接(SynchronousQueue):任务不多时,只需要用队列进行简单的任务中转,这种队列无法存储任务,在使用这种队列时,需要将maxPoolSize设置的大一点。

    • 无界队列(LinkedBlockingQueue):如果使用无界队列当作runnableTaskQueue,将maxQueue设置的多大都没有用,使用无界队列的优点是可以防止流量突增,缺点是如果处理任务的速度跟不上提交任务的速度,这样就会导致无界队列中的任务越来越多,从而导致OOM异常。

    • 有界队列(ArrayBlockingQueue):使用有界队列可以设置队列大小,让线程池的maxPoolSize有意义。

  3. maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。 如果队列满了, 并且已创建的线程数小于最大线程数, 则线程池会再创建新的线程执行任务。 值得注意的是, 如果使用了无界的任务队列这个参数就没什么效果。

  4. ThreadFactory: 用于设置创建线程的工厂, 可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字。

  5. RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。 这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略:
      AbortPolicy:直接抛出异常;
      CallerRunsPolicy:只用调用者所在线程来运行任务;
      DiscardOldestPolicy:丢弃队列里最近的一个任务, 并执行当前任务;
      DiscardPolicy:不处理,丢弃掉。

  6. keepAliveTime(线程活动保持时间): 线程池的工作线程空闲后, 保持存活的时间。 所以,如果任务很多, 并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

  7. TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、 微秒(MICROSECONDS, 千分之一毫秒) 和纳秒(NANOSECONDS, 千分之一微秒)。

线程池的任务队列选用LinkedBlockingQueue时,测试代码如下:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolTest implements Runnable {

    @Override
    public void run() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        BlockingQueue<Runnable> queue = new LinkedBlockingDeque<Runnable>();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 1,
                TimeUnit.DAYS, queue);
        for (int i = 0; i < 10; i++) {
            executor.execute(new Thread(new ThreadPoolTest(), "TestThread"
                    .concat("" + i)));
            int threadSize = queue.size();
            System.out.println("线程队列大小为-->" + threadSize);
        }
        executor.shutdown();
    }
}

Java中的线程池分类(这个是Excutors工具类封装好的, 缺少很多方法, 不建议使用)

在这里我们介绍一下Java中四种具有不同功能常见的线程池。他们都是直接或者间接配置ThreadPoolExecutor来实现他们各自的功能。这四种线程池分别是newFixedThreadPool,newCachedThreadPool,newScheduledThreadPool和newSingleThreadExecutor。这四个线程池可以通过Executors类获取。下面分别介绍这四种线程池。

1. newFixedThreadPool

我们可以通过Executors中的newFixedThreadPool方法来创建,该线程池是一种线程数量固定的线程池。在这个线程池中所容纳最大的线程数就是我们设置的核心线程数。如果线程池的线程处于空闲状态的话,它们并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就回处于等待状态,直到有线程空闲出来。由于newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是它能够更快速的响应外界请求。从下面的newFixedThreadPool方法的实现可以看出,newFixedThreadPool只有核心线程,并且不存在超时机制,采用LinkedBlockingQueue,所以对于任务队列的大小也是没有限制的。

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

我们可以通过Executors中的newCachedThreadPool方法来创建,通过下面的newCachedThreadPoolfan’f在这里我们可以看出它的核心线程数为0,线程池的最大线程数Integer.MAX_VALUE。而Integer.MAX_VALUE是一个很大的数,也差不多可以说这个线程池中的最大线程数可以任意大。当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60秒,所以当线程处于闲置状态超过60秒的时候便会被回收。这也就意味着若是整个线程池的线程都处于闲置状态超过60秒以后,在newCachedThreadPool线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。对于newCachedThreadPool他的任务队列采用的是SynchronousQueue,上面说到在SynchronousQueue内部没有任何容量的阻塞队列。SynchronousQueue内部相当于一个空集合,我们无法将一个任务插入到SynchronousQueue中。所以说在线程池中如果现有线程无法接收任务,将会创建新的线程来执行任务。

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

我们可以通过Executors中的newScheduledThreadPool方法来创建,它的核心线程数是固定的,对于非核心线程几乎可以说是没有限制的,并且当非核心线程处于限制状态的时候就会立即被回收。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
4. newSingleThreadExecutor

我们可以通过Executors中的newSingleThreadExecutor方法来创建,在这个线程池中只有一个核心线程,对于任务队列没有大小限制,也就意味着这一个任务处于活动状态时,其他任务都会在任务队列中排队等候依次执行。newSingleThreadExecutor将所有的外界任务统一到一个线程中支持,所以在这个任务执行之间我们不需要处理线程同步的问题。

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

最后不管你用的何种方法, 注意关闭资源

executor.shutdown();
发布了255 篇原创文章 · 获赞 428 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_33709508/article/details/105484047
今日推荐