Java 并发进阶知识之线程池

一、为什么要使用线程

线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。

使用线程池的好处:

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

二、如何创建线程池?

(1) ThreadPoolExecutor 创建线程池

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler);

创建线程池需要如下的参数:

  1. corePoolExecutor:核心线程池的基本大小。
  2. runnableTaskQueue:任务队列,用于保存等待执行任务的阻塞队列。有以下几个可供选择:
    • ArrayBlockingQueue:是一个基于数据结构的有界阻塞队列,此队列按照 FIFO 原则对元素进行排序;
    • LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列是按 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列;
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入元素一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executor.newCachedThreadPool 使用了这个队列;
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  3. maxmumPoolSize:线程池最大数量。
  4. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  5. RejectedExecutionHandle(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务是抛出异常。在 JDK1.5 中 Java 线程池框架提供了一下 4 中策略:
    • AbortPolicy:直接抛出异常;
    • CallerRunPolicy:只用调用者所在线程来运行任务;
    • DiscardOldestPolicy:丢弃队列任务里最近的一个任务,并执行当前任务;
    • DiscardPolicy:不处理,丢弃掉。

也可以根据应用场景来实现 RejectedExecutionHandle 接口自定义策略。如记录日志或持久化存储不能处理的任务。

  • keepAliveTime:线程活动保持时间,线程池的工作线程空闲后,保持存活的时间。所以如果任务很多的话,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。只有当线程池中的线程数大于corePoolSize时,这个参数才会起作用
  • TimeUnit:线程活动保持时间的单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

(2) Executor 创建线程池

a、newFixedThreadPool

FixedThreadPool 被称为可重用固定线程数的线程池。如下源代码:

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

  1. 如果当前运行的线程数少于 corePoolSize,则创建新线程来执行任务。
  2. 在线程完成预热后(当前运行的线程数等于 corePoolSize),将任务加入 LinkedBlockingQueue。
  3. 线程执行完 1 中的任务后,会在循环中反复从 LinkedBlockingQueue 获取任务来执行。

 FixedThreadPool 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列容量为 Integer.MAX_VALUE)。使用无界队列会造成如下影响:

  1. 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize。
  2. 由于 1,使用无界队列 maxmumPoolSize 将是一个无效参数。
  3. 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数。
  4. 由于使用无界队列,运行中的 FixedThreadPool 不会拒绝任务。

b、newSingleThreadExecutor() 单线程线程池

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

SingleThreadExecutor() 是使用单个 worker 线程的 Executor。单线程线程池,那么线程池中运行的线程数肯定是1。 workQueue 选择了无界的 LinkedBlockingQueue,那么不管来多少任务都排队,前面一个任务执行完毕,再执行队列中的线程。从这个角度讲,第二个参数 maximumPoolSize 是没有意义的,因为 maximumPoolSize 描述的是排队的任务多过 workQueue 的容量,线程池中最多只能容纳 maximumPoolSize 个任务,现在 workQueue 是无界的,也就是说排队的任务永远不会多过 workQueue 的容量,那 maximum 其实设置多少都无所谓了。

  1. 如果当前运行的线程数少于 corePoolSize(即线程池中无运行的线程),则创建一个新的线程来执行任务。
  2. 线程完成预热后(当前运行的线程数等于 corePoolSize),将任务加入 LinkedBlockingQueue。
  3. 线程执行完 1 中的任务后,会在一个无限循环中反复从 LinkedBlockingQueue 获取任务。

c、newCachedThreadPool

CachedThreadPool 是一个会根据需要创建新线程的线程池。如下源代码:

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

corePoolSize 被设置为 0;maximumPoolSize 被设置为 Integer.MAX_VALUE,即 maximumPool 是无界的。将 keepAliveTime 设置为 60L,表明空闲线程等待新任务的时间最长为 60s,超过后则会被终止。

CachedThreadPool 使用没有容量的 SynchronousQueue 作为线程池的工作队列,但是 maximumPool 确是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新线程。极端情况下,CachedThreadPool 会因为创建过多的线程而耗尽 CPU 和内存资源。

  1. 首先执行 SynchronousQueue.offer(Runnable task)。如果当前 maximumPool 中有空闲线程正在执行 SynchronousQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute() 方法执行完成;否则执行步骤 2。
  2. 当初始maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime, TimeUnit.NANOSECODS)。这种情况下步骤 1 将失败。此时 CachedThreadPool 会创建一个新线程执行任务,execute() 方法执行完成。
  3. 在步骤 2 中新创建的线程将任务执行完后,会执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个 poll 操作会让空闲线程最多在SynchronousQueue 中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的 CachedThreadPool 不会使用任何资源。

第 4 种:newScheduledThreadPool

创建固定长度的线程池,且同时以延迟或者定时的方法来执行任务。

三、阻塞队列 BlockingQueue

该类主要提供了两个方法 put() 和 take(),前者将一个对象放到队列中,如果队列以及满了,就等待直到有空闲节点;与后者从 head 去一个对象,如果没有对象,就等待直到有可取的对象。

FixedThreadPool 与 Sing了ThreadPool 都是采用无界的 LinkedBlockingQueue 实现。LinkedBlockingQueue 中引入了两把锁 takeLock 和 putLock,显然分别是用于 take 操作和 put 操作的。即 LinkedBlockingQueue 入队和出队用的是不同的锁,那么 LinkedBlockingQueue 可以同时进行入队和出队操作,但是由于使用链表实现,所有查找速度会慢一些。

CachedThreadPool 使用的是 SynchronousQueue。

线程池对任务队列包括三种:有界队列、无界队列、同步移交。

  • 无界队列:当请求不断增加时队列将无限增加,因此会出现资源耗尽的情况。
  • 有界队列:如 LinkedBlockingQueue,ArrayBlockingQueue 等,可以避免资源耗尽的情况,但是可能出现队列填满后新任务如何处理?执行饱和策略:中止,抛出异常;抛弃:抛弃该任务;调用者运行:将任务返回给调用者。一般任务的大小和线程池的大小一起调节。对于非常大的队列或者无界队列,里面的任务可能会长时间排队等待。可以直接使用同步移交交任务给工作者线程执行。同步移交并不是真正的队列,只是一种在线程之间移交的机制。

猜你喜欢

转载自www.cnblogs.com/reformdai/p/11099978.html