ExecutorService介绍

参考:https://blog.csdn.net/fwt336/article/details/81530581

前言

        在开发中为了提高系统的响应速度和处理能力会使用到多线程,但线程的创建和释放,需要占用不小的内存和资源。如果每次需要使用线程时,都new 一个Thread的话,难免会造成资源的浪费,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源导致系统瘫痪。不利于扩展。就像MySQL数据库连接一样,每创建一个连接都需要消耗资源,所以就引入了数据库连接池,线程也引入了线程池的概念,需要线程时可以不用创建,直接从池中获取,在JDK中就为我们提供了ExecutorService。

ExecutorServiceExecutorService 提供了几种不同类型的线程池,包括单线程池、固定大小线程池、可缓存线程池和定时任务线程池。通过这些线程池,我们可以有效地管理多个任务的执行,并且可以控制线程池的大小,可以有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,同时提供定时执行、定期执行、单线程、并发数控制等功能,也不用使用TimerTask了。

1. ExecutorService的创建方式

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

所有线程池最终都是通过这个方法来创建的。

corePoolSize : 核心线程数,一旦创建将不会再释放。如果创建的线程数还没有达到指定的核心线程数量,将会继续创建新的核心线程,直到达到最大核心线程数后,核心线程数将不在增加;达到核心线程数但没有空闲的核心线程,同时又未达到最大线程数,则将继续创建非核心线程;如果核心线程数等于最大线程数,则当核心线程都处于激活状态时,任务将被挂起,等待空闲线程来执行。

maximumPoolSize : 最大线程数,允许创建的最大线程数量。如果最大线程数等于核心线程数,则无法创建非核心线程;如果非核心线程处于空闲时,超过设置的空闲时间,则将被回收,释放占用的资源。

keepAliveTime : 也就是当线程空闲时,所允许保存的最大时间,超过这个时间,线程将被释放销毁,但只针对于非核心线程。

unit : 时间单位,TimeUnit.SECONDS等。

workQueue : 任务队列,存储暂时无法执行的任务,等待空闲线程来执行任务。

threadFactory : 线程工程,用于创建线程。

handler : 当线程边界和队列容量已经达到最大时,用于处理阻塞时的程序

2.线程池的类型

2.1 可缓存线程池

ExecutorService cachePool = Executors.newCachedThreadPool();

看看它的具体创建方式:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, //核心线程数
   								  Integer.MAX_VALUE,  //线程池大小
                                  60L,  //空闲保存时间
                                  TimeUnit.SECONDS, 
                                  new SynchronousQueue<Runnable>() //阻塞队列);
}

通过它的创建方式可以知道,创建的都是非核心线程,而且最大线程数为Interge的最大值,空闲线程存活时间是1分钟。如果有大量耗时的任务,则不适该创建方式。它只适用于生命周期短的任务。

2.2 单线程池

ExecutorService singlePool = Executors.newSingleThreadExecutor();

顾名思义,也就是创建一个核心线程:

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

只用一个线程来执行任务,保证任务按FIFO顺序一个个执行。

2.3 固定线程数线程池

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

也就是创建固定数量的可复用的线程数,来执行任务。当线程数达到最大核心线程数,则加入队列等待有空闲线程时再执行。

2.4 固定线程数,支持定时和周期性任务

ExecutorService scheduledPool = Executors.newScheduledThreadPool(5);
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

可用于替代handler.postDelay和Timer定时器等延时和周期性任务。

问题:最大线程数量为Integer最大值,不就是可以创建很多个空闲线程用来处理任务吗?为什么会任务还会被放入工作队列中等待处理

      在 ScheduledThreadPoolExecutor 的构造方法中,将最大线程数量设置为 Integer.MAX_VALUE。这意味着线程池可以创建非常多的空闲线程来处理任务。然而,即使存在大量空闲线程,仍然会将任务放入工作队列中等待处理的原因是为了减少线程创建和销毁的开销,并且能够更好地控制线程的数量。通过工作队列,线程池可以根据任务的到达速率和线程的处理能力来动态调整任务的执行顺序和并发度,以保证任务能够按时得到执行。因此,虽然 ScheduledThreadPoolExecutor 可以创建大量的空闲线程,但为了更好地管理和控制线程数量,任务仍然会被放入工作队列中等待处理,以实现更高效的任务调度和执行。

public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

scheduleAtFixedRatesheduleWithFixedDelay有什么不同呢?

scheduleAtFixedRate: 用于以固定的时间间隔执行任务。它接受一个 Runnable 类型的参数以及两个 long 类型的参数:initialDelay(初始延迟时间)和 period(任务执行的时间间隔)。该方法会在 initialDelay 时间后开始执行第一次任务,然后每隔 period 时间执行一次。如果任务的执行时间超过了指定的时间间隔 period,那么下一次任务的执行会立即开始,不会等待上一次任务的完成。这意味着任务的执行可能会重叠。

sheduleWithFixedDelay: 在任务执行完成后,等待 delay 时间后再开始下一次任务的执行。这样可以确保任务之间有固定的时间间隔,并且不会重叠执行。

2.5 手动创建线程池

private ExecutorService pool = new ThreadPoolExecutor(3, 10,
            10L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(512), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

可以根据自己的需求创建指定的核心线程数和总线程数。

3. 应用场景

  1. 并发任务执行:ExecutorService 可以用于执行并发的异步任务,通过线程池的方式,可以有效地管理和复用线程资源,提高任务执行的效率。
  2. 定时任务调度:ExecutorService 可以用于定时执行任务,通过调用 schedule() 或 scheduleAtFixedRate() 方法,可以实现按照指定的时间间隔或固定频率执行任务的功能。
  3. 大规模数据处理:当需要对大规模数据进行处理时,可以将数据分割成多个任务,并提交给 ExecutorService 执行,以并发的方式对数据进行处理,提高处理速度。

4. 代码案例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentTaskExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        // 提交 10 个任务给线程池执行
        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executorService.submit(() -> {
                System.out.println("任务 " + taskId + " 开始执行");
                // 模拟任务执行耗时
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务 " + taskId + " 执行完成");
            });
        }

        // 关闭线程池
        executorService.shutdown();

		try {
            // 等待所有任务完成或超时(这里设置超时时间足够大)
            if (!executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)) {
                // 如果超时仍有任务未完成,则强制关闭线程池
                executorService.shutdownNow();
                System.out.println("等待超时,强制关闭线程池");
            }
        } catch (InterruptedException e) {
            // 捕获中断异常
            e.printStackTrace();
            executorService.shutdownNow();
        }
    }
}

结果任务不是按进来无序完成的,任务1耗时并不会阻塞程序对任务2的处理。

5. 线程池要关闭吗

观点一:关闭好
在上面的代码中需要关闭线程池,线程池的关闭可以确保线程池中的线程在不再需要时被正确地释放和销毁。不关闭线程池可能导致一些问题:

  1. 资源泄漏: 如果不关闭线程池,它将一直保持活动状态,并且线程池中的线程将继续存在。这会导致资源的浪费,特别是在长时间运行的应用程序中。

  2. 系统负载: 线程池中的线程将占用系统资源,包括内存和处理器。如果不关闭线程池,这些资源将被持续占用,可能导致系统负载增加。

因此,为了避免以上问题,建议在不再需要线程池时,显式地调用 shutdown() 方法来关闭线程池。这将停止线程池接受新的任务,并开始逐渐关闭线程池中的线程,直到所有任务都执行完毕。

之前部署过一个SpringBoot项目在云服务器上,总是运行一阵子服务器就会宕机,内存占满了,虽然我的项目采用固定线程数的线程池,但即使线程池的线程数是固定的,也会造成资源的消耗。

观点二:不关好
大多使用的场景并非上述示例那样朝生夕死。线程池一般是持续工作的全局场景,如数据库连接池。 线程池在项目中是需要持续工作的全局场景,不建议手动关闭线程池(具体结合自己的项目场景)。我们创建线程池的目的就是反复利用线程池里的线程,如果频繁创建和关闭线程池,解决了内存泄露的问题,但是失去了使用线程池的意义。

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private AdditionService additionService;
    

    //在线程池中创建50个线程
    private ExecutorService es = Executors.newFixedThreadPool(50);

    @Override
    public void saveUser(String name) {
        Addition one = additionService.getOne(name);
        if (one == null) {              
                Runnable task = () -> additionService.queryDetail(word);
                es.submit(task);
        }
    }
 }

在这个示例中,ExecutorService是在UserServiceImpl类中定义的一个成员变量,并且是在类初始化时创建的。这意味着在每个UserServiceImpl实例中只会创建一个ExecutorService实例,而不是每次方法调用都会创建一个新的线程池。UserServiceImpl是一个单例(@Service注解默认为单例),则所有请求都将共享同一个ExecutorService实例。因此,每次调用saveWord方法时,都会使用已经创建的ExecutorService实例来执行任务。这个ExecutorService实例中已经包含了50个线程,可以用来处理多个请求。

6. 缺点

ExecutorService是JDK自带的线程创建类,如果ExecutorService创建的线程数量过多,可能导致内存溢出的问题。每个线程都需要占用一定的内存资源,包括栈空间、线程上下文等。当线程数量过多时,会消耗大量的内存资源。此外,过多的线程也可能导致CPU资源的浪费,因为线程上下文切换的开销会增加。这可能会降低程序的性能并增加系统负载。

因此,在使用ExecutorService创建线程池时,应该合理估计系统的资源容量,并根据需求和系统能力来决定线程池的大小,以避免内存溢出和性能问题。

你可能感兴趣的:(Java进阶,java)