Skip to content

ThreadPoolExecutor——线程池

更新: 1/11/2025 字数: 0 字 时长: 0 分钟

引入

在我们使用数据库连接时,如果每次对数据库进行一次操作都要完成一次连接,这样是十分消耗资源和时间的。

在这种情况下,我们会发现每次操作的连接和关闭是固定的,那么我们是否在完成一次数据库操作之后暂时不关闭连接而是让这一线程等待下一次数据库操作呢?

当然是可以的,这就需要线程池来帮助我们实现这个过程。

为什么要使用线程池?

线程池可以对线程进行统一的分配调优和监控

如何使用?

JDK自带一套线程池生成策略,通过Executor提供

这里我们来看一个简单的例子(WorkThread是一个手动创建的实现了Runnable接口的类)

java
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(5); //这里的数字表示线程池的最大线程数量 
        for (int i=0;i<10;i++){     //这里我们提交了十个线程,但由于线程池最大为5,因此会先执行前五个线程,其他线程需要等待别的线程执行结束
            service.submit(new WorkThread());    //向线程池中提交线程
        }
        service.shutdown();  //使用该方法后,线程池不再运行submit任何线程并且会执行完毕剩余线程,然后关闭线程池(此时若再submit会产生异常)
        while (!service.isTerminated()){   //isTerminated方法会返回一个boolean类型,表示当前线程池中的线程是否全部运行完毕
            //一个小型的自旋
        }
        System.out.println("FinishAll!");
    }

在示例代码中,Executor类为我们提供了一个可以直接使用的线程池服务,我们只需指定最大线程数便可使用。

JUC提供的线程池

newFixedThreadPool

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

手动创建线程池效果会更好

当我们使用Executor并使用默认的线程池创建时,我们会发现Alibaba代码规范中有这样一个提示 手动创建线程池效果会更好。

Alibaba代码规范建议我们提供一个详细的线程池创建方式,那我们该如何操作呢

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

我们发现使用new的方式创建线程池会有七个参数,让我们分别介绍一下这七个参数

  1. corePoolSize——核心线程数:当我们提交任务时,线程池会新建一个线程执行该任务,直到到当前线程数等于corePoolSize, 这时就算是前面的线程运行完了,如果线程池中线程数量未达到corePoolSize,那么仍然会继续创建线程。 若线程数量达到corePoolSize,则新来线程会保存在阻塞队列中,直到有空余线程可以给他执行。 若在之前使用了prestartAllCoreThreads()方法,则会一次性创建多个空线程填满corePoolSize
  2. maximumPoolSize——最大线程数:若阻塞队列已满,则创建新的线程执行任务,但若此时线程数大于maximumPoolSize则交给Handler去处理
  3. keepAliveTime——线程最大存活时间:大于corePoolSize的线程会有一个存活时间,一旦超过这个时间,则该线程终止,下次使用需要重新创建
  4. TimeUnit——时间单位:keepAliveTime的单位
  5. workQueue——阻塞队列:我们可以自定义阻塞队列
    1. ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO排序任务
    2. LinkedBlockingQueue: 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue
    3. SynchronousQueue: 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue;
    4. PriorityBlockingQueue: 具有优先级的无界阻塞队列,当使用该队列时maximumPoolSize不生效(永远不会满)
  6. ThreadFactory——线程创建方式:可不填,默认为DefaultThreadFactory
  7. handler——多余线程处理器:ThreadPoolExecutor中存在4个内部类作为默认策略,可以通过new的形式填充在这里
    1. AbortPolicy: 直接抛出异常,当不填时使用这个;
    2. CallerRunsPolicy: 用调用者所在的线程(使用该线程池的线程)来执行任务;
    3. DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
    4. DiscardPolicy: 直接丢弃任务;
    5. 自定义一个类,实现RejectedExecutionHandler接口/或使用lambda表达式
java
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {//两个参数分别是当先线程池和多出来的线程
        System.out.println(r.toString()+" is Rejected——Core is"+executor.getCorePoolSize());
        System.out.println(executor);
    }
}

线程的启动方式

使用线程池启动一个线程存在submit()和execute()两种方式,我们区分一下。

特性submitexecute
入参支持Runnable和Callable只接受Runnable的实现类
返回值返回Future对象无返回值
异常异常在Future中,可单独处理异常由线程池自己处理

由此我们不难发现,submit更适合有返回值或是需要特别处理异常的任务

线程池的关闭方式

shutdown 使用shutdown方法会终止所有未执行的线程,然后等待当前线程执行完

shutdownNow 使用shutdown方法会终止所有的线程

状态

  1. 当调用shutdown或shutdownNow后,我们调用 isShutDown 方法会返回true
  2. 当全部线程都关闭了,isTerminated才会返回true
本站访客数 人次      本站总访问量