Java为什么不建议使用Executors来创建线程池呢?

2024年 2月 28日 82.3k 0

我们都知道在面试的过程中,关于线程池的问题,一直都是面试官比较注重的考点,现在也不会有面试官会选择去问创建线程都有哪些方式了,而更多的实惠关注到如何去使用线程池,今天了不起就来和大家说说线程池。

Java创建线程池方式

在Java中,创建线程池主要使用java.util.concurrent包下的Executors类。这个类提供了几种静态工厂方法,用于创建和管理不同类型的线程池。以下是一些常见的创建线程池的方式:

1.Fixed Thread Pool(固定线程池)

  • 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。
  • 创建方法:Executors.newFixedThreadPool(int nThreads)

2.Cached Thread Pool(缓存线程池)

  • 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
  • 创建方法:Executors.newCachedThreadPool()

3.Single Thread Executor(单线程执行器)

  • 创建一个使用单个工作线程的 Executor,以无界队列方式来运行该线程。(注意,如果单个线程始终因为等待新任务而处于非活动状态,则在现行线程终止之前,它可能无法终止。)但是,如果线程因为失败而终止,那么会有一个新的线程来替代它。单个线程的优势在于,你无需处理对线程生命周期的管理。
  • 创建方法:Executors.newSingleThreadExecutor()

4.Scheduled Thread Pool(计划线程池)

  • 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
  • 创建方法:Executors.newScheduledThreadPool(int corePoolSize)

5.自定义线程池

除了使用Executors类提供的静态工厂方法创建线程池外,还可以通过实例化ThreadPoolExecutor类来自定义线程池。这种方式提供了更多的灵活性,允许你设置线程池的核心参数,如核心线程数、最大线程数、线程存活时间、任务队列等。

示例代码:

import java.util.concurrent.*;  
  
public class CustomThreadPool {  
    public static void main(String[] args) {  
        int corePoolSize = 5;  
        int maximumPoolSize = 10;  
        long keepAliveTime = 60L;  
        TimeUnit unit = TimeUnit.SECONDS;  
        BlockingQueue workQueue = new LinkedBlockingQueue();  
        ThreadFactory threadFactory = Executors.defaultThreadFactory();  
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();  
  
        ThreadPoolExecutor executor = new ThreadPoolExecutor(  
                corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);  
  
        // 使用线程池执行任务...  
    }  
}

非自定义线程池的缺点

我们先来看看 Executors 当中的几个方法,也就是上面了不起给大家写的除了自定义线程池的几个方法。

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

在源码中有一个类,我们明显的看到了队列的身影,那就是 LinkedBlockingQueue。

它实现了一个基于链接节点的可选容量的阻塞队列。此队列按 FIFO(先进先出)排序元素。队列的头部是在队列中存在时间最长的元素,队列的尾部是在队列中存在时间最短的元素。新元素总是插入到队列的尾部,而检索操作(如 take 和 poll)总是从队列的头部开始。

LinkedBlockingQueue 是一个线程安全的队列,它内部使用了锁和条件变量来保证多线程环境下的正确性和一致性。因为它是阻塞队列,所以它可以用于生产者和消费者模型,在生产者线程和消费者线程之间传递数据。

LinkedBlockingQueue 的主要特点就几个

  • 容量可选
  • 阻塞操作
  • 非阻塞操作
  • 线程安全
  • 高效的并发性能

为什么说容量可选呢?因为我们如果单独使用这个LinkedBlockingQueue 那么你可以在创建 LinkedBlockingQueue 时指定一个容量,这将限制队列中可以存储的元素数量。如果未指定容量,则队列的容量将是 Integer.MAX_VALUE。当队列满时,任何尝试插入元素的线程都将被阻塞,直到队列中有空间可用。

而阻塞操作则是他提供了阻塞的 put 和 take 方法。put 方法用于添加元素到队列中,如果队列已满,则调用线程将被阻塞直到队列有空闲空间。take 方法用于从队列中移除并返回头部元素,如果队列为空,则调用线程将被阻塞直到队列中有元素可用。

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node node = new Node(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        ......
        

public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
.....

我们看一个使用LinkedBlockingQueue的示例:

import java.util.concurrent.BlockingQueue;  
import java.util.concurrent.LinkedBlockingQueue;  
  
public class ProducerConsumerExample {  
    public static void main(String[] args) throws InterruptedException {  
        BlockingQueue queue = new LinkedBlockingQueue(5);  
  
        Thread producer = new Thread(() -> {  
            try {  
                for (int i = 0; i  {  
            try {  
                while (true) {  
                    Integer item = queue.take();  
                    System.out.println("Consumed: " + item);  
                    Thread.sleep(500); // 模拟消费耗时  
                }  
            } catch (InterruptedException e) {  
                Thread.currentThread().interrupt();  
            }  
        });  
  
        producer.start();  
        consumer.start();  
  
        producer.join();  
        // 注意:这里的 consumer 线程是一个无限循环,所以它不会自然结束。  
        // 在实际应用中,你需要有一个明确的停止条件来结束消费者线程。  
    }  
}

说到这里感觉说多了,我们回归正题,如果我们使用标准的 newCachedThreadPool 方法,如果线程数设置和任务数不能够配合起来,就比如说设置的线程数是一定的,这个时候,任务数量越多,就会慢慢的进入到队列LinkedBlockingQueue中,队列的话,任务越多,占用的内存越多,最终就非常容易耗尽内存,导致OOM。

所以我们不推荐直接使用 Executors 来创建线程池,但是我们更推荐使用 ThreadpoolExecutor创建线程池。原因就是如下的几点:

1.资源控制:ThreadPoolExecutor 允许你明确控制并发线程的最大数量,防止因为创建过多的线程而耗尽系统资源。通过合理地设置线程池的大小,可以平衡资源利用率和系统性能。

2.线程复用:线程池中的线程可以被多个任务复用,这减少了在创建和销毁线程上花费的时间以及开销,提高了系统的响应速度。

3.任务队列:ThreadPoolExecutor 内部维护了一个任务队列,当线程池中的线程都在工作时,新提交的任务会被放在队列中等待执行。这提供了一种缓冲机制,可以平滑处理突发的高并发任务。

4.灵活性:ThreadPoolExecutor 提供了多种配置选项,如核心线程数、最大线程数、线程存活时间、任务队列类型等,这些选项可以根据具体的应用场景进行调整,以达到最佳的性能和资源利用率。

5.异常处理:当线程池中的线程因为未捕获的异常而终止时,ThreadPoolExecutor 会创建一个新的线程来替代它,从而保持线程池的稳定性。此外,你也可以通过提供自定义的 ThreadFactory 来控制线程的创建过程,例如设置线程的名称、优先级、守护状态等。

6.可扩展性:ThreadPoolExecutor 的设计是基于策略的,它使用了多个接口和抽象类来定义线程池的行为,这使得它很容易通过扩展或替换某些组件来适应不同的需求。

7.与Java并发库集成:ThreadPoolExecutor 是 Java 并发库 java.util.concurrent 的一部分,这个库提供了丰富的并发工具和类,如锁、信号量、倒计时器、阻塞队列等,这些都可以与 ThreadPoolExecutor 无缝集成,简化多线程编程的复杂性。

8.性能监控和调优:ThreadPoolExecutor 提供了一些有用的方法,如 getTaskCount()、getCompletedTaskCount()、getPoolSize() 等,这些方法可以帮助你监控线程池的运行状态,从而进行性能调优。

所以你了解了么?

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论