Java进阶篇--线程池之ThreadPoolExecutor

目录

为什么要使用线程池

线程池的创建

线程池执行逻辑

线程池的关闭

线程池的工作原理

线程池阻塞队列

线程池的饱和策略

代码示例

如何配置线程池参数?


为什么要使用线程池

在实际使用中,线程是很占用系统资源的,如果对线程管理不善很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有以下好处:

  1. 降低资源消耗:通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗。创建和销毁线程是相对昂贵的操作,线程池可以通过重用空闲线程,减少创建新线程的开销,提高系统性能。
  2. 提升系统响应速度:通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度。线程池在初始化时会提前创建一定数量的线程,并将它们置于等待状态,当有任务到来时,可以直接从线程池中取出一个线程去执行,避免了频繁创建和销毁线程的开销。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以对线程进行统一的管理和调度,设置合适的线程池大小可以限制同时执行的线程数量,从而控制系统对CPU、内存等资源的占用。
  4. 防止任务过载:线程池可以根据系统负载情况动态调整线程数量,如果系统的负载较重,线程池可以增加线程数量以提高并发处理能力;如果系统的负载较轻,线程池可以减少线程数量以释放资源。通过控制线程池大小可以更好地适应不同负载下的任务处理需求,避免系统因任务过载而崩溃或响应缓慢。
  5. 提供线程管理和监控:线程池可以提供线程的管理和监控功能,例如线程的状态、活跃线程数、完成任务数等信息。这些信息可以用于系统的调试、性能优化和监控,方便定位和解决线程相关的问题。

综上所述,使用线程池可以降低资源消耗,提升系统响应速度,提高线程的可管理性,并且可以防止任务过载,同时还能提供线程管理和监控功能。这些优势使得线程池在多线程应用中被广泛使用,能够有效地提升系统的性能、效率和可靠性。

线程池的创建

创建线程池主要使用ThreadPoolExecutor类,具有多个重载的构造方法。以下是通过最多参数的构造方法来理解创建线程池时需要配置的参数:

ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue workQueue,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)

下面对每个参数进行说明:

1、corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池中的线程数没有达到corePoolSize,即使存在空闲线程,也会创建新的线程来执行任务。如果调用了prestartCoreThread()或prestartAllCoreThreads()方法,在线程池创建时所有核心线程将被创建并启动。

2、maximumPoolSize:表示线程池能够创建的最大线程数。当阻塞队列已满时,如果当前线程池中的线程数没有超过maximumPoolSize,则会创建新的线程来执行任务。

3、keepAliveTime:空闲线程的存活时间。如果当前线程池中的线程数超过了corePoolSize,且空闲时间超过了keepAliveTime,则会销毁这些空闲线程,以降低系统资源消耗。

4、unit:时间单位,用于指定keepAliveTime的时间单位。

5、workQueue:阻塞队列,用于保存任务。可以使用 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue等不同实现来作为阻塞队列。

6、threadFactory:线程工厂,用于创建线程。可以通过指定线程工厂来为每个创建出来的线程设置有意义的名称,便于调试和问题排查。

7、handler:饱和策略,用于处理线程池已达到饱和状态时的情况。当线程池的阻塞队列已满且所有线程都已经开启时,表示线程池已经饱和。可选的策略包括:

  • AbortPolicy:直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
  • CallerRunsPolicy:由调用线程直接执行该任务;
  • DiscardPolicy:直接丢弃掉任务,不做任何处理;
  • DiscardOldestPolicy:丢弃阻塞队列中存放时间最久的任务,然后执行当前任务。

以上是使用最多参数的构造方法来创建线程池时需要配置的参数。根据具体的业务需求和性能要求,可以灵活调整这些参数来达到最佳的线程池配置。

线程池执行逻辑

使用execute(Runnable task)方法提交一个Runnable任务给线程池执行,或者使用submit(Callable task)方法提交一个Callable任务给线程池执行,返回一个Future对象用于获取任务的执行结果。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
        int c = ctl.get();
	//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
	//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
	//如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务
    else if (!addWorker(command, false))
        reject(command);
}

注意:execute方法的执行逻辑包括以下几个情况:

  • 当线程池中正在运行的线程数量小于核心线程数(corePoolSize)时,尝试创建一个新的线程来执行当前任务。这里使用addWorker方法进行原子性地检查运行状态和工作线程数量,以防止在不应该添加线程的情况下出现误报错。如果成功添加了新的线程,则方法直接返回。
  • 如果无法创建新的线程,或者线程数量已经达到了核心线程数的上限,则将提交的任务放入阻塞队列workQueue中。
  • 如果阻塞队列已满,则尝试创建一个新的线程来执行任务。
  • 如果线程池的线程数量已经达到了最大线程数(maximumPoolSize)的限制,并且无法添加新的线程,则根据设定的饱和策略(RejectedExecutionHandler)来处理任务。饱和策略可以根据具体需求进行自定义,常见的策略包括直接拒绝、抛出异常等。

线程池的设计思想是通过合理利用有限的资源(核心线程数和阻塞队列)来处理任务,避免频繁创建和销毁线程带来的额外开销,提高系统的性能和资源利用率。这种设计思想被广泛应用于各种框架和系统中。

线程池的关闭

shutdown()方法和shutdownNow()方法是用于关闭线程池的两个常用方法。

当调用shutdown()方法时,线程池的状态会被设置为SHUTDOWN,此时线程池将不再接受新的任务提交,但会继续执行已提交的任务,直到所有任务执行完成。该方法不会阻塞,会立即返回。

而调用shutdownNow()方法时,线程池的状态会被设置为STOP,线程池会尝试停止所有正在执行和未执行的任务,并返回等待执行的任务列表。该方法会中断所有工作线程,包括正在执行任务的线程,并返回一个包含未执行任务的列表。同样地,该方法也不会阻塞,会立即返回。

当调用了其中一个关闭方法后,可以通过isShutdown()方法判断线程池是否已经关闭,它会返回一个布尔值,表示线程池是否处于关闭状态。只有在所有的任务都执行完毕并且线程池处于关闭状态时,调用isTerminated()方法会返回true,表示线程池已经完全终止。

需要注意的是,在调用这两个关闭方法后,若还有任务在等待执行或者线程池中的任务无法正常终止,可以使用其他手段来处理,比如通过awaitTermination()方法等待一段时间,若超过指定时间还未终止,则可以选择强制退出。

关闭线程池是合理释放资源和保证系统稳定的重要操作,因此在使用完线程池后,一定要记得关闭线程池。

线程池的工作原理

线程池的工作原理可以总结为以下几个步骤:

1、线程池判断核心线程池中的线程是否都在执行任务。如果还有空闲的线程,则从核心线程池中选取一个线程来执行新提交的任务。如果线程池中的线程都在执行任务,进入下一步。

2、线程池判断阻塞队列是否已满。如果阻塞队列未满,将新提交的任务放入阻塞队列中等待执行。阻塞队列起到了任务缓冲的作用,当线程池中的线程都在执行任务时,新提交的任务会被暂存到阻塞队列中,等待线程池中的线程空闲下来再执行。

3、如果阻塞队列已满,意味着线程池无法容纳更多的任务。这时,线程池会根据事先设置的饱和策略来处理新提交的任务。常见的饱和策略有:

  • AbortPolicy(默认):抛出RejectedExecutionException异常;
  • CallerRunsPolicy:由提交任务的线程来执行该任务;
  • DiscardOldestPolicy:丢弃阻塞队列中最旧的任务,然后尝试把新提交的任务放入队列;
  • DiscardPolicy:直接丢弃新提交的任务,不做任何处理。

以上是线程池的基本工作原理。线程池可以提高并发任务的执行效率和线程资源的利用率,通过合理设置线程池的大小和饱和策略,可以更好地适应系统的负载情况,提高系统的稳定性和性能。同时,线程池还提供了监控、统计以及任务取消和异常处理等机制,对于大规模并发任务的处理非常有用。

线程池阻塞队列

阻塞队列在线程池中的作用是用来存储等待执行的任务。它可以根据任务的先后顺序进行排序,通常按照先入先出的原则或者根据任务的优先级进行排序。

常见的阻塞队列有:

  • ArrayBlockingQueue:基于数组实现的有界阻塞队列,按照先入先出的原则排序。它的容量是固定的,不支持动态扩容。由于其实现简单,使用较少。
  • PriorityBlockingQueue:支持优先级的无界阻塞队列。任务可以根据优先级进行排序,具有更高优先级的任务会被先执行。这个队列使用较少,一般在特定需求下使用。
  • LinkedBlockingQueue:基于链表实现的有界阻塞队列,默认最大长度是Integer.MAX_VALUE。和ArrayBlockingQueue一样,也是按照先入先出的原则进行排序。它的吞吐量通常要高于ArrayBlockingQueue,因此在线程池中使用较多。
  • SynchronousQueue:不存储元素的阻塞队列,每个put操作都必须等待一个take操作,否则无法继续添加元素。这个队列适用于生产者和消费者模型,吞吐量较高,所以在线程池中也使用较多。

需要注意的是,SynchronousQueue中每个插入操作都必须等待一个对应的移除操作,否则插入操作将一直处于阻塞状态。

通过选择不同类型的阻塞队列,可以根据具体的需求来平衡吞吐量、公平性和任务执行顺序。常见的线程池实现类如Executors.newFixedThreadPool()和Executors.newCachedThreadPool()会使用不同的队列实现,根据具体的场景进行选择。

线程池的饱和策略

线程池的饱和策略是在队列和线程池都满载的情况下,决定如何处理新提交的任务。常见的饱和策略包括AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy和DiscardPolicy。

  • AbortPolicy(中断策略):当线程池和队列都满了,新提交的任务会直接抛出RejectedExecutionException异常。这是默认的饱和策略。
  • CallerRunsPolicy(调用者运行策略):当线程池和队列都满了,新提交的任务会由调用线程来执行。也就是说,如果线程池无法处理新任务,那么提交任务的线程将自己来执行该任务。
  • DiscardOldestPolicy(舍弃最旧任务策略):当线程池和队列都满了,新提交的任务会丢弃队列中最旧的任务,然后重试任务的提交执行。这样可以保证新提交的任务得到执行,但可能会丢失一些较旧的任务。
  • DiscardPolicy(舍弃策略):当线程池和队列都满了,新提交的任务会被直接丢弃,不做任何处理。

除了以上常见的饱和策略,我们还可以自定义饱和策略。通过实现RejectedExecutionHandler接口并重写rejectedExecution方法,我们可以自定义任务被拒绝时的处理逻辑。例如,可以记录日志、将任务存储到持久化队列中等。

需要根据具体的业务需求和系统特点选择合适的饱和策略。不同的策略可能会对系统的性能、稳定性和可靠性产生不同的影响。

总结:线程池提供了灵活、高效地管理和调度多线程任务的机制,能够提升系统性能、减少资源消耗。通过合理设置线程池的参数、选择合适的阻塞队列和饱和策略,可以根据实际需求来配置线程池,提高任务处理的效率和可靠性。

代码示例

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

public class main {
    public static void main(String[] args) {
        // 核心线程数
        int corePoolSize = 5;
        // 最大线程数
        int maximumPoolSize = 10;
        // 空闲线程存活时间
        long keepAliveTime = 5000; // 5秒
        // 阻塞队列
        BlockingQueue workQueue = new ArrayBlockingQueue<>(100);

        // 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                TimeUnit.MILLISECONDS,
                workQueue
        );

        // 提交任务给线程池执行
        for (int i = 0; i < 20; i++) {
            threadPool.execute(new Task(i));
        }

        // 关闭线程池
        threadPool.shutdown();
    }

    static class Task implements Runnable {
        private int taskId;

        public Task(int taskId) {
            this.taskId = taskId;
        }

        @Override
        public void run() {
            System.out.println("执行任务:" + taskId);
        }
    }
}

在这个示例中,我们创建了一个ThreadPoolExecutor对象来管理线程池。通过设置核心线程数、最大线程数、空闲线程存活时间和阻塞队列等参数,来配置线程池的行为。

然后,我们使用execute方法提交任务给线程池执行。这里创建了20个任务,每个任务都是一个实现了Runnable接口的Task对象。

最后,通过调用shutdown方法来关闭线程池,等待所有任务执行完毕。注意,shutdown方法只是停止接收新任务,并且会等待已提交的任务执行完成。

以上就是一个使用ThreadPoolExecutor的简单示例,通过合理配置线程池参数,可以更好地控制线程池的行为和性能。

如何配置线程池参数?

合理配置线程池参数是确保线程池能够高效运行的重要步骤。

  • 根据任务类型和系统负载进行调整:首先要考虑任务的类型和特性,以及系统的负载情况。不同类型的任务可能对线程池的参数需求不同。例如,CPU密集型任务可能需要较少的线程数,而IO密集型任务可能需要更多的线程数。
  • 确定核心线程数:核心线程数是线程池中一直保持存活的线程数量。根据任务的性质和系统负载,确定一个适当的核心线程数。通常情况下,可以根据CPU的核心数来设定核心线程数,但也要考虑到IO等待时间和任务的并发性。
  • 设置最大线程数:最大线程数是线程池中允许的最大线程数量。根据系统负载和资源限制,设置一个合适的最大线程数。过高的最大线程数可能会导致资源耗尽,而过低的最大线程数可能会导致任务堆积。
  • 配置任务队列大小:任务队列用于存储还未执行的任务。根据任务的数量和特性,选择合适的队列类型和大小。如果任务数量较大或者任务执行时间较长,可以选择一个较大的队列来缓冲任务。
  • 设置线程空闲时间:线程空闲时间是指当线程处于空闲状态时,等待新任务到来的最长时间。根据任务的特性和系统负载,设置一个合适的线程空闲时间。过长的空闲时间可能会导致资源浪费,而过短的空闲时间可能会频繁地创建和销毁线程。
  • 考虑拒绝策略:在线程池饱和时,需要有一种合理的拒绝策略来处理新提交的任务。根据业务需求和系统特点,选择适合的拒绝策略。
  • 监控和调优:配置好线程池参数后,需要进行监控和调优。通过监控线程池的运行情况,例如线程池的活跃线程数、任务队列长度等指标,及时调整线程池的参数以适应系统的变化和需求。

总之,合理配置线程池参数需要综合考虑任务类型、系统负载、资源限制和业务需求等因素。通过不断试验和调整,找到最优的配置参数,以提高系统的性能和稳定性。

你可能感兴趣的:(Java进阶篇,1024程序员节,开发语言,java)