Java中的线程池怎么创建?ThreadPoolExecutor 构造方法的参数是何含义?

63 阅读4分钟

1.线程池

1.1 线程池是什么

  线程池就是 事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟线程的时间,提高了代码执行效率。

  那么线程池的出现是要解决什么样的问题呢?

  1. 降低线程创建和销毁的开销: 线程的创建和销毁是一项开销较大的操作。如果在每次需要执行任务时都创建新的线程,会导致系统性能下降。
  2. 控制并发线程数量: 在高并发的情况下,如果不限制线程数量,可能会导致资源过度消耗,甚至出现系统崩溃的情况。线程池可以控制并发线程的数量,防止系统资源被耗尽。
  3. 提供线程管理和监控: 线程池提供了线程的生命周期管理和监控,可以更好地控制线程的状态、健康情况和执行情况。

1.2 Java 标准库中的线程池

  在Java标准库中,提供了一个用于管理线程池的Executor框架,它位于java.util.concurrent包下。

Executors 类:这是一个包含一些静态工厂方法的辅助类,用于创建不同类型的线程池实例,比如:

  • newFixedThreadPool(int nThreads):创建一个固定大小的线程池,其中包含指定数量的线程。
  • newCachedThreadPool():创建一个缓存线程池,线程数可以根据任务数量的变化自动调整。
  • newSingleThreadExecutor():创建一个单线程的线程池,只有一个线程在工作,用于顺序执行任务。
  • newScheduledThreadPool(int corePoolSize):创建一个可以执行定时任务的线程池。这个方法返回一个 ScheduledExecutorService 对象,它是 ExecutorService 接口的子接口,专门用于支持定时任务的执行。参数 corePoolSize 是指定线程池的核心线程数,它表示在没有定时任务执行时,线程池会保持的线程数量。

ExecutorService 接口:这是线程池的主要接口,定义了一些常用的线程池方法,比如submit()用于提交任务、shutdown()用于关闭线程池等。

public class ThreadPoolDemo {
    public static void main(String[] args) {
        //创建一个拥有 10 个线程的线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //提交 1000 个任务
        for (int i = 0; i < 1000; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("n:" + n);
                }
            });
        }
    }
}

1.2.1 ThreadPoolExecutor 构造方法的参数

  Executors 本质上是 ThreadPoolExecutor 类的封装,比如在Executors 中:

image.png

image.png

image.png

   ThreadPoolExecutor 最长的构造方法如下:

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

   它们都是些什么含义呢?

  1. corePoolSize: 核心线程池大小,即线程池中保持的线程数。即使线程是空闲的,也不会被回收。这些线程会一直存在,除非线程池关闭。
  2. maximumPoolSize: 线程池中允许的最大线程数,包括核心线程和非核心线程。超过核心线程数的线程,如果空闲时间超过 keepAliveTime,则会被回收,直到线程数不超过核心线程数。
  3. keepAliveTime: 非核心线程的空闲时间,超过这个时间,非核心线程会被回收。只有在线程池中线程数量超过核心线程数时,这个参数才会起作用。
  4. unit: 空闲时间的时间单位,例如 TimeUnit.SECONDS 表示秒,TimeUnit是枚举类型。
  5. workQueue: 存放任务的阻塞队列。待执行的任务会被放入这个队列,等待线程池中的线程执行。
  6. threadFactory: 线程工厂,用于创建线程。可以自定义线程的创建方式,例如设置线程名、线程优先级等。
  7. handler: 拒绝策略,用于处理任务添加到线程池被拒绝的情况。当线程池中的线程数量已达到最大线程数,且阻塞队列已满时,新提交的任务将根据指定的拒绝策略进行处理。
    • ThreadPoolExecutor.AbortPolicy(默认):该策略会直接抛出 RejectedExecutionException 异常,表示拒绝执行新的任务。这是默认的拒绝策略,当线程池的任务队列和线程池都已满时,新的任务将被拒绝。
    • ThreadPoolExecutor.CallerRunsPolicy:该策略不会抛出异常,而是将被拒绝的任务交给调用线程来执行。也就是说,如果线程池的任务队列和线程池都已满,执行任务的线程将会尝试执行被拒绝的任务。
    • ThreadPoolExecutor.DiscardPolicy:该策略会默默地丢弃被拒绝的任务,不会抛出任何异常,也不会执行任务。如果对任务丢失无关紧要,可以选择此策略。
    • ThreadPoolExecutor.DiscardOldestPolicy:该策略会丢弃任务队列中最老的一个任务(即队列头部的任务),然后尝试提交当前被拒绝的任务。这样做的目的是让线程池尽可能保留更近提交的任务。
    public static void main(String[] args) {
        // 核心线程池大小为 2,最大线程池大小为 5,非核心线程的空闲时间为 1 秒
        int corePoolSize = 2;
        int maximumPoolSize = 5;
        long keepAliveTime = 1;
        TimeUnit unit = TimeUnit.SECONDS;

        // 使用 LinkedBlockingQueue 作为任务队列,最大容量为 10
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10);

        // 自定义线程工厂,用于设置线程名称
        ThreadFactory threadFactory = new ThreadFactory() {
            private int counter = 0;

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread-" + counter++);
            }
        };

        // 自定义拒绝策略,当任务添加到线程池被拒绝时,在调用者线程中直接执行任务
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();

        // 创建 ThreadPoolExecutor 对象
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );
        
        //……
    }

1.3 简单模拟实现线程池

  我们这里实现一下线程池的核心代码。这里模拟一个固定大小的线程池。

public class MyThreadPool {
    //用阻塞队列来存储任务
    private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    //n 表示线程池中的线程数量
    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                while(true){
                    try {
                        //如果队列中没有元素则阻塞
                        Runnable runnable = queue.take();
                        //执行任务
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }

    //添加任务
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  这里的模拟得很简单,但是这个思想很重要。