Java中的线程池

线程池

线程池就是管理一系列线程的资源池,其提供了一种限制和管理线程资源的方式。 使用线程池的好处:

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

使用场景:线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。

ThreadPoolExecutor类介绍

    /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
  • maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor 拒绝策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用者运行策略,线程池中没办法运行,那么就由提交任务的这个线程运行
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求,也就是最早进入阻塞队列的任务。

创建线程池的两种方式:

1.通过ThreadPoolExecutor构造函数创建

2.通过 Executor 框架的工具类 Executors 来创建。(不推荐)

线程池示例代码

public class ThreadPoolExecutorDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            Runnable work = new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
                    processCommand();
                    System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
                }
            };
            executor.execute(work);
        }
        //终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");

    }
    private static void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

可以看到上面的代码指定了:

  • corePoolSize: 核心线程数为 5。
  • maximumPoolSize:最大线程数 10
  • keepAliveTime : 等待时间为 1L。
  • unit: 等待时间的单位为 TimeUnit.SECONDS。
  • workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
  • handler:拒绝策略为 CallerRunsPolicy

输出:

pool-1-thread-4 Start. Time = Tue Jul 30 21:47:13 GMT+08:00 2024
pool-1-thread-3 Start. Time = Tue Jul 30 21:47:13 GMT+08:00 2024
pool-1-thread-1 Start. Time = Tue Jul 30 21:47:13 GMT+08:00 2024
pool-1-thread-5 Start. Time = Tue Jul 30 21:47:13 GMT+08:00 2024
pool-1-thread-2 Start. Time = Tue Jul 30 21:47:13 GMT+08:00 2024
pool-1-thread-5 End. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-5 Start. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-3 End. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-2 End. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-4 End. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-1 End. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-4 Start. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-3 Start. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-2 Start. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-1 Start. Time = Tue Jul 30 21:47:18 GMT+08:00 2024
pool-1-thread-4 End. Time = Tue Jul 30 21:47:23 GMT+08:00 2024
pool-1-thread-2 End. Time = Tue Jul 30 21:47:23 GMT+08:00 2024
pool-1-thread-5 End. Time = Tue Jul 30 21:47:23 GMT+08:00 2024
pool-1-thread-1 End. Time = Tue Jul 30 21:47:23 GMT+08:00 2024
pool-1-thread-3 End. Time = Tue Jul 30 21:47:23 GMT+08:00 2024
Finished all threads//任务全部执行完了才会跳出来,因为executor.isTerminated()判断为true了才会跳出while循环,当且仅当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

执行流程:

线程类对比

  • Runnable 被线程执行,没有返回值也无法抛出异常
  • Callable 可以获取执行结果,无法得出结果时抛出异常。

execute() 和 submit()

execute() 和 submit()是两种提交任务到线程池的方法,有一些区别:

  • 返回值:execute() 方法用于提交不需要返回值的任务。通常用于执行 Runnable 任务,无法判断任务是否被线程池成功执行。submit() 方法用于提交需要返回值的任务。可以提交 Runnable 或 Callable 任务。submit() 方法返回一个 Future 对象,通过这个 Future 对象可以判断任务是否执行成功,并获取任务的返回值(get()方法会阻塞当前线程直到任务完成, get(long timeout,TimeUnit unit)多了一个超时时间,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException)。
  • 异常处理:在使用 submit() 方法时,可以通过 Future 对象处理任务执行过程中抛出的异常;而在使用 execute() 方法时,异常处理需要通过自定义的 ThreadFactory (在线程工厂创建线程的时候设置UncaughtExceptionHandler对象来 处理异常)或 ThreadPoolExecutor 的 afterExecute() 方法来处理

阻塞队列

有界队列:有固定大小的队列,一开始设定的初始大小
无界对立:没有固定大小,可以一直添加(直到超出Integer.MAX_VALUE) java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:

  • FIFO 队列
  • ArrayBlockQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)的阻塞队列
  • PriorityBlockQueue:支持优先级排序的无界阻塞队列
  • DelayedWorkQueue:使用优先级队列实现的延迟无界阻塞队列
  • SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个 put 的线程放入元素为止
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列
end

评论