线程池

155 阅读14分钟

线程池是什么?

  • 线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。

使用线程池的好处

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布
  • 导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池的工作原理

线程池的主要组件

  1. 线程池****管理器(ThreadPoolExecutor:线程池管理器的主要作用是管理线程池的生命周期,包括创建和销毁线程,管理任务队列和工作线程。
  2. 工作队列(BlockingQueue):工作队列用于存储已经提交但还未执行的任务。当一个任务被提交给线程池时,如果没有空闲的工作线程,该任务就会被放在工作队列中等待。
  3. 工作线程:工作线程是线程池中真正执行任务的线程。当有任务提交给线程池时,工作线程会从工作队列中取出任务并执行。
  4. 任务接口(Runnable/Callable:任务接口定义了任务的入口,每个任务都必须实现这个接口。Runnable接口的任务没有返回值,而Callable接口的任务有返回值。

工作流程

  1. 任务提交:当一个外部任务(通常是一个实现了Runnable接口的对象)提交给线程池后,它首先会被存放在一个任务队列中。这个队列也被称为工作队列。
  2. 任务执行:线程池中的工作线程会不断地从任务队列中取出待处理任务并执行。如果所有的线程都在忙,新来的任务就会等待在任务队列中。
  3. 线程的复用:一旦线程完成了任务的执行,它会返回到线程池中,然后开始处理下一个等待在队列中的任务。这种方式有效地重复使用了线程,避免了频繁创建和销毁线程带来的性能开销。
  4. 线程池****关闭:当不再需要线程池时,可以调用其shutdown()或shutdownNow()方法来关闭线程池。这会等待正在执行的任务完成,然后关闭线程池。需要注意的是,shutdownNow()会试图立即停止所有正在执行的任务,然后关闭线程池。

img

线程池的创建

七个创建方法

  • 线程池的创建方法有七种,但大体来说可以分为两类:通过Executors创建和通过ThreadPoolExecutor创建
  • Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待; Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程; Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序; Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池; Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池; Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。 new ThreadPoolExecutor**(...)**:最原始的创建线程池的方式,它包含了 7 个参数可供设置
  • 上述线程池的使用场景:
    • **FixedThreadPool:**固定大小线程池适用于服务器端的程序,需要控制资源的使用,防止由于线程过多而导致系统负荷过大。
    • **SingleThreadExecutor:**适用于串行执行任务场景
    • **CachedThreadPool:**执行大量短生命周期任务。因为maximumPoolSize是无界的,所以提交任务的速度 > 线程池中线程处理任务的速度就要不断创建新线程;每次提交任务,都会立即有线程去处理,因此CachedThreadPool适用于处理大量、耗时少的任务。
    • **ScheduledThreadPool:**周期性执行任务,并且需要限制线程数量的场景
  • 线程池的意义:虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。

ThreadPoolExecutor的七个参数

  • 参数 1:corePoolSize 核心线程数,线程池中始终存活的线程数。
  • 参数 2:maximumPoolSize 最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
  • 参数 3:keepAliveTime 最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
  • 参数 4:unit: 单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:

​ TimeUnit.DAYS:天 TimeUnit.HOURS:小时 TimeUnit.MINUTES:分 TimeUnit.SECONDS:秒 TimeUnit.MILLISECONDS:毫秒 TimeUnit.MICROSECONDS:微妙 TimeUnit.NANOSECONDS:纳秒

  • 参数 5:workQueue
    • 阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
    • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。 LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。 SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。 PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。 DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。 LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。 LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
    • 较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue有关。
    • 特点和使用场景
      • ArrayBlockingQueue:
        • 基于数组实现,队列容量固定。存/取数据的操作共用一把锁(默认非公平锁),无法实现真正意义上存/取操作并行执行。
        • 由于基于数组,容量固定所以不容易出现内存占用率过高,但是如果容量太小,取数据比存数据的速度慢,那么会造成过多的线程进入阻塞。因此适用于基本没有并发和吞吐量的要求,操作没那么频繁的的业务。
      • LinkedBlockingQueue:
        • LinkedBlockingQueue基于链表实现,队列容量默认Integer.MAX_VALUE, 存/取数据的操作分别拥有独立的锁,可实现存/取并行执行。
        • 1.基于链表,数据的新增和移除速度比数组快,但是每次存储/取出数据都会有Node对象的新建和移除,所以也存在由于GC影响性能的可能 2.默认容量非常大,所以存储数据的线程基本不会阻塞,但是如果消费速度过低,内存占用可能会飙升。 3.读/取操作锁分离
        • 适用于生产消费速度差不多的场景
      • PriorityBlockingQueue:
        • 基于数组实现,队列容量最大为Integer.MAX_VALUE - 8(减8是因为数组的对象头)。根据传入的优先级进行排序,保证按优先级来消费
        • 优先级阻塞队列中存在一次排序,根据优先级来将数据放入到头部或者尾部,排序带来的损耗因素,由二叉树最小堆排序算法来降低。适用于业务上存在优先级的场景
      • DelayQueue:
        • DelayQueue延迟队列,基于优先级队列来实现,存储元素必须实现Delayed接口(Delayed接口继承了Comparable接口)
        • 由于是基于优先级队列实现,但是它比较的是时间,我们可以根据需要去倒叙或者正序排列(一般都是倒叙,用于倒计时)。适用于类似超时取消的业务场景
      • SynchronousQueue:
        • 采用双栈双队列算法的无空间队列或栈 任何一个对SynchronousQueue写需要等到一个对SynchronousQueue的读操作,任何一个个读操作需要等待一个写操作 没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者。
        • 相当于是交换通道,不存储任何元素,提供者和消费者是需要组队完成工作,缺少一个将会阻塞线程,直到等到配对为止
  • **参数 6:**threadFactory 线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。
    • 不传入时默认为jdk自带的DefaultThreadFactory
    • jdk还自带了一个PrivilegedThreadFactory,可以创建一个与当前线程具有相同权限的新线程
      • Executors 类的 privilegedThreadFactory() 方法返回一个线程工厂,用于创建与当前线程具有相同权限的新线程。该工厂使用与 defaultThreadFactory() 相同的设置创建线程,另外将新线程的 AccessControlContext 和 contextClassLoader 设置为与调用此特权线程工厂方法的线程相同。
    • 也可以自己实现一个自定义线程工厂(继承ThreadFactory类)
  • 参数 7:handler 拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:

​ AbortPolicy:拒绝并抛出异常。 CallerRunsPolicy:使用当前调用的线程来执行此任务。 DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。 DiscardPolicy:忽略并抛弃当前任务。 默认策略为 AbortPolicy。

向线程池提交任务

  • 通过两种方法(executor和submit)可以向线程池提交任务,两者略有不同

executor

execute 方法是 Executor 接口的一个方法,用于提交一个 Runnable 任务到线程池。这个方法没有返回值。

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("Task is running.");
    }
});

submit

submit 方法是 ExecutorService 接口的一个方法,它可以提交 Runnable 或 Callable 任务到线程池,并返回一个 Future 对象,可以用于获取任务的结果或取消任务。

ExecutorService executor = Executors.newFixedThreadPool(10);
Future<?> future = executor.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("Runnable task is running.");
    }
});

// submit method with a Callable
Future<Integer> futureWithResult = executor.submit(new Callable<Integer>() {
    @Override
    public Integer call() {
        return 1 + 1;
    }
});

try {
    Integer result = futureWithResult.get(); // it will return 2
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}   

关闭线程池

  • ExecutorService提供的shutdown方法和shutdownNow方法可以关闭线程池,shutdown方法关闭线程池不会中断正在进行的任务,而shutdownNow会立刻中断正在执行的任务。

线程池的监控和优化

  • Java 中提供的 ThreadPoolExecutor 类提供了多个方法来帮助我们获取线程池的当前状态。这些方法包括:
    • getPoolSize(): 返回线程池中的当前线程数。
    • getActiveCount(): 返回线程池中当前正在执行任务的线程数。
    • getCompletedTaskCount(): 返回线程池已完成的任务数。
    • getTaskCount(): 返回线程池已接收的任务总数。
    • getQueue(): 返回线程池中的任务队列,包括正在等待执行的任务。
  • 线程池的调优需要综合考虑CPU,内存,系统响应时间等多方面因素,以下是三个基本的调优策略
    • 调整线程池大小:线程池的大小对于其性能有显著影响。如果线程池太大,将会产生过多的上下文切换,浪费 CPU 资源。如果线程池太小,CPU 的使用率可能无法达到最大化。线程池的大小通常取决于系统的硬件资源(如CPU的数量)和任务的性质(CPU 密集型或IO 密集型)。
    • 调整任务队列长度:任务队列长度也会影响线程池的性能。如果队列过长,将会增加任务的等待时间,从而影响系统的响应时间。如果队列过短,可能会因为队列满而拒绝新的任务。
    • 设置合适的拒绝策略:当任务队列满而无法接受新的任务时,线程池会执行拒绝策略。选择合适的拒绝策略可以更好地处理这种情况,防止系统过载。
    • 根据任务性质调整线程池大小:
    • 1.CPU 密集型任务(N+1) 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
    • 2.I/O 密集型任务(2N) 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

线程池异常处理

  • 在处理线程池的异常时,我们需要理解:在 Runnable 对象的 run() 方法中,异常不能跨线程传播回主线程。这是因为每个线程都有自己的堆栈,互相之间不会影响。这就意味着如果你没有在你的 Runnable 类中正确处理异常,那么这个异常会被吞掉,并且你可能永远也不知道这个异常的存在。
    • 解决:创建线程的同时向thread传入一个Thread.UncaughtExceptionHandler 接口的实现类,这个类中可以实现对异常的处理方法

    • public class TaskWithExceptionHandler implements Runnable {
          @Override
          public void run() {
              throw new RuntimeException("Exception from thread");
          }
      }
      
      ================创建thread然后传入UncaughtExceptionHandler接口实现类============
      Thread thread = new Thread(new TaskWithExceptionHandler());
      thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
          @Override
          public void uncaughtException(Thread t, Throwable e) {
              System.out.println("Caught " + e);
          }
      });
      thread.start();
      
    • 但由于线程池有自己的工作线程,我们是无法直接设置UncaughtExceptionHandler的,所以我们可以实现一个工厂类,在线程池每次创建线程时设置。

线程池任务结果处理

使用 Future 获取结果

Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以及获取计算的结果和取消计算的方法。当我们将一个 Callable 任务提交给 ExecutorService,我们会得到一个 Future 对象。通过这个对象的get()方法可以获取结果

处理 FutureTask 的结果

FutureTask 是 Future 的一个实现,它同时实现了 Runnable 接口,所以可以直接提交给 ExecutorService。FutureTask 也可以用来包装 Callable 或 Runnable 对象。

Callable<String> task = () -> {
    Thread.sleep(1000);
    return "Result from callable";
};

FutureTask<String> futureTask = new FutureTask<>(task);
executor.submit(futureTask);

try {
    String result = futureTask.get();
    System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

使用 CompletionService 处理任务的结果

如果你有很多的并发任务需要执行,并且需要处理这些任务的结果,ExecutorCompletionService 是一个好的选择。ExecutorCompletionService 是 CompletionService 的一个实现,它可以将 Executor 和 BlockingQueue 功能结合在一起。

ExecutorService executor = Executors.newFixedThreadPool(5);
CompletionService<String> completionService = new ExecutorCompletionService<>(executor);

for (int i = 0; i < 5; i++) {
    int finalI = i;
    completionService.submit(() -> {
        Thread.sleep(finalI * 1000);
        return "Result from task " + finalI;
    });
}

for (int i = 0; i < 5; i++) {
    try {
        String result = completionService.take().get();
        System.out.println(result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

自定义线程池

  • 面对不同的业务要求时我们有时候需要自定义线程池,通过修改在构造ThreadPoolExecutor时传入的参数即可实现自定义线程池。

线程池的设计模式

  • 工厂模式:封装和管理对象的创建,为创建对象提供接口,以此隔离创建对象的具体过程
  • 享元模式:实现对象的共享,当系统中对象多的时候可以减少内存的开销