threadpoolexecutor创建线程池_Java并发如何优雅地创建线程池

@本文阅读大概需要6分钟 c1f90b1506259d30614be7520f1fe4a1.png

1 为何要线程池化

线程池是一种池化技术,目的是避免线程频繁的创建和销毁带来的性能消耗。它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。

《Java 并发编程艺术》一书中提到使用线程池的好处:

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

常见的池化技术:Tomcat 线程池、数据库连接池、HTTP 连接池等。

2 如何创建线程池

Java 中创建线程池有以下两种方式:

  • 通过 ThreadPoolExecutor 类创建(推荐)
  • 通过 Executors 类创建

其实这两种方式在本质上是一种方式,都是通过 ThreadPoolExecutor 类的方式创建,因为 Exexutors 类调用了 ThreadPoolExecutor 类的方法。

2.1 ThreadPoolExecutor 方式

查看 JDK1.8 的源码,ThreadPoolExecutor 类源码有四个构造函数:

961548ed7b6211ca4e23f6ae3b742b10.png

ThreadPoolExecutor 类在 java.util.concurrent 包下,部分源码:

//这个包太迷人,面试官太喜欢问里面的知识点
package java.util.concurrent;
public class ThreadPoolExecutor extends AbstractExecutorService {

   //七个参数的构造函数
   public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize             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;
    }
    //...其他
}

1. 参数 corePoolSize

  • 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值;
  • 应该结合实际业务设置此值的大小。若 corePoolSize 的值较小,则会出现频繁创建和销毁线程情况;若值较大,则会浪费系统资源。

2. 参数 maximumPoolSize

  • 表示线程池最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize 的值;
  • 此值只有在任务比较多,且不能存放在任务队列时,才会用到。

3. 参数 keepAliveTime

  • 表示线程的存活时间。当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数等于 corePoolSize 的值为止;
  • 若 maximumPoolSize 的值 等于 corePoolSize 的值,则线程池在空闲的时候不会销毁任何线程。

4. 参数 unit

  • 表示存活时间的单位,配合 keepAliveTime 参数共同使用。

5. 参数 workQueue

  • 表示线程池执行的任务队列;
  • 当线程池的所有线程都在处理任务时,若来了新任务则会缓存到此任务队列中,然后等待执行。

6. 参数 threadFactory

  • 表示线程的创建工厂,一般使用默认的线程创建工厂的方法 Executors.defaultThreadFactory()来创建线程。

7. 参数 RejectedExecutionHandler

  • 表示指定线程池的拒绝策略,属于一种限流保护的机制;
  • 当线程池的任务已经在缓存队列 workQueue 中存满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略
  • 四种拒绝策略
    (1) AbortPolicy: 丢弃任务并抛出异常。
    (2) DiscardPolicy:丢弃任务但不抛出异常。
    (3) DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务
    (4) CallerRunsPolicy:由调用线程处理该任务

2.2 Executors 方式

查看 JDK1.8 的源码,Executors 类源码有 12 个创建线程的静态方法:

df659b6dcad85b9c6c95264b0404cd44.png

Executors 类在 java.util.concurrent 包下,部分源码:

package java.util.concurrent;
public class Executors {
    //参数一般使用默认的,对编程不可见
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }//...其他
}

从源码看来,Exexutors 类本质上调用了 ThreadPoolExecutor 类的构造方法,实现线程的创建。

3 线程池如何工作

3.1 创建线程池

先来看下具体如何创建线程池,撸一把代码:

package com.coding.wbp;

import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPool {

  public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(20), new ThreadPoolExecutor.CallerRunsPolicy());

    for (int i = 1; i 5; i++) {
      // 创建WorkerThread对象
      Runnable worker = new MyThreadPool().new MyRunnable("" + i);
      // 执⾏任务
      executor.execute(worker);
    }

  }

  class MyRunnable implements Runnable {

    private String name;

    public MyRunnable(String name) {
      this.name = name;
    }

    private void process() {
      try {
        Thread.sleep(3000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    @Override
    public void run() {
      System.out.println("Thread-" + name + " -> start... " + new Date());
      process();
      System.out.println("Thread-" + name + " -> end... " + new Date());
    }
  }

}

运行结果:

Thread-1 -> start... Tue Jul 07 22:08:14 CST 2020
Thread-2 -> start... Tue Jul 07 22:08:14 CST 2020
Thread-1 -> end... Tue Jul 07 22:08:17 CST 2020
Thread-3 -> start... Tue Jul 07 22:08:17 CST 2020
Thread-2 -> end... Tue Jul 07 22:08:17 CST 2020
Thread-4 -> start... Tue Jul 07 22:08:17 CST 2020
Thread-3 -> end... Tue Jul 07 22:08:20 CST 2020
Thread-4 -> end... Tue Jul 07 22:08:20 CST 2020

结果分析:
模拟了 4 个任务,配置的核心线程数 corePoolSize 为 2,等待任务容量为 20,所以每次只可能存在 2 个任务在并行执行,剩下的 2 个任务存放在任务队列中等待执行。当前 2 个任务执行完,后 2 个任务开始执行。

3.2 执行流程

线程池开始工作,通过执行方法 execute()开始,此方法源码:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    //Proceed in 3 steps:
    int c = ctl.get();
    
    //Step1 当前工作的线程数小于核心线程数corePoolSize值
    if (workerCountOf(c)         // 则创建新的线程执行此任务
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    //Step2 检查线程池是否处于运行状态,如果是则把任务添加到队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 如果是非运行状态,则将刚加入队列的任务移除
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 如果线程池的线程数为 0 时(corePoolSize 为 0 )
        else if (workerCountOf(recheck) == 0)
            // 新建线程执行任务
            addWorker(null, false); 
    }

    //Step3 核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败
    else if (!addWorker(command, false)) 
        // 执行拒绝策略
        reject(command);
}

线程池任务执行的主要工作流程:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当执行 execute() 方法添加一个任务时,线程池会判断:(a) 若正在运行的线程数量小于 corePoolSize 值,则立刻创建线程运行此任务;(b) 若正在运行的线程数量大于或等于 corePoolSize 值,则将此任务放入队列;(c) 若此时队列满了,而且正在运行的线程数量小于 maximumPoolSize 值,则创建非核心线程立刻运行这个任务;(d) 若队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize 值,那么线程池会执行设置的拒绝策略。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程空闲时,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize 值,那么这个线程会被销毁。

线程池任务执行的主要流程图如下:

0bd68a05ac3655dc5fa277979523afac.png

4 常见面试题

4.1 execute 与 submit 方法区别

执行线程池任务方法有 execute() 和 submit()方法,看个Demo:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ThreadPoolExecutor executor =
        new ThreadPoolExecutor(5, 8, 10L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(15),
            Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    executor.execute(new Runnable() {
      @Override
      public void run() {
        System.out.println("execute method.");
      }
    });

    Future future = executor.submit(new Callable() {@Overridepublic String call() throws Exception {
        System.out.println("submit method.");return "hello";
      }
    });
    System.out.println("value: " + future.get());
  }

运行结果:

execute method.
submit method.
value: hello

两者主要区别:

  1. submit() 方法可以配合 Future来接收线程执行的返回值,而 execute() 不能接收返回值;
  2. execute() 方法属于 Executor 接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法。
7c4dc026facb5b86c5dd47ef3ab33f9d.png

每天进步一点点,前进不止一小点efc96e3d548e164aef87b383fdf8bfe6.png

动动手指关注哦a4c44c12872a3c77798b365044c94509.png

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,以下是用 Java 的 ThreadPoolExecutor 创建线程池的示例代码: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; public class ThreadPoolExample { public static void main(String[] args) { int corePoolSize = 2; int maximumPoolSize = 4; long keepAliveTime = 10; ExecutorService executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, java.util.concurrent.TimeUnit.SECONDS, new java.util.concurrent.LinkedBlockingQueue<Runnable>() ); // 提交任务 for (int i = 0; i < 10; i++) { executor.execute(new Task(i)); } // 关闭线程池 executor.shutdown(); } static class Task implements Runnable { private int id; public Task(int id) { this.id = id; } public void run() { System.out.println("Task " + id + " is running."); } } } ``` 在这个例子中,我们使用 ThreadPoolExecutor 创建了一个线程池。它的参数包括: - corePoolSize:线程池中核心线程的数量; - maximumPoolSize:线程池中最大线程数; - keepAliveTime:线程池中非核心线程的超时时间; - TimeUnit:超时时间的单位; - BlockingQueue:用于缓存等待执行的任务的队列。 我们还定义了一个 Task 类来模拟需要执行的任务。在主函数中,我们提交了 10 个任务给线程池,并在任务执行结束后关闭了线程池。 ### 回答2: 使用java中的ThreadPoolExecutor类可以很方便地创建线程池。首先,我们需要导入java.util.concurrent包,在代码中创建ThreadPoolExecutor对象。 ThreadPoolExecutor类的构造函数有多个参数,其中最重要的是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue和threadFactory。corePoolSize指定了线程池中保留的线程数,maximumPoolSize指定了线程池中允许的最大线程数。keepAliveTime和unit用于指定超过corePoolSize数量的空闲线程在被终止之前等待新任务的最长时间。workQueue用于存储等待执行的任务。threadFactory用于创建线程。 下面是一个简单的示例代码: ``` import java.util.concurrent.*; public class ThreadPoolExample { public static void main(String[] args) { int corePoolSize = 5; int maximumPoolSize = 10; long keepAliveTime = 1; TimeUnit unit = TimeUnit.SECONDS; BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10); ThreadFactory threadFactory = Executors.defaultThreadFactory(); ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); // 添加任务到线程池 for (int i = 0; i < 20; i++) { executor.execute(new Task(i)); } // 终止线程池 executor.shutdown(); } } class Task implements Runnable { private int taskId; public Task(int taskId) { this.taskId = taskId; } @Override public void run() { System.out.println("Task " + taskId + " is running."); } } ``` 在上面的示例中,我们创建了一个ThreadPoolExecutor对象executor,它使用corePoolSize=5、maximumPoolSize=10、keepAliveTime=1秒、单位TimeUnit.SECONDS的时间单位、workQueue容量为10的ArrayBlockingQueue以及默认的线程工厂。然后,我们向线程池中添加20个任务,每个任务都会打印任务的ID。最后,我们通过executor.shutdown()方法来终止线程池。 通过使用ThreadPoolExecutor类,我们可以方便地创建和管理线程池,从而实现任务的并发执行。 ### 回答3: Java中的ThreadPoolExecutor是一个用于创建和管理线程池的类。 首先,我们需要导入java.util.concurrent包中的ThreadPoolExecutor类。接下来,我们可以使用ThreadPoolExecutor的构造函数来创建一个线程池对象。构造函数接受一些参数,如核心线程数、最大线程数、线程等待时间等。 例如,我们可以使用以下代码创建一个线程池对象: ``` ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); ``` 在上面的代码中,我们创建了一个核心线程数为5,最大线程数为10的线程池。执行的任务将被放入一个无界队列LinkedBlockingQueue中,并且线程空闲时等待的时间为60秒。 接下来,我们可以通过调用execute()方法来提交任务给线程池执行。例如,我们可以创建一个实现了Runnable接口的任务,并将其提交给线程池执行,如下所示: ``` Runnable task = new MyTask(); executor.execute(task); ``` 上述代码中,MyTask是一个实现了Runnable接口的自定义任务类。我们创建了一个MyTask对象,并使用execute()方法将其提交给线程池执行。 最后,当不再需要使用线程池时,我们可以调用shutdown()方法来关闭线程池。例如: ``` executor.shutdown(); ``` 在上述代码中,executor.shutdown()方法将使线程池停止接受新的任务,并等待所有已提交的任务执行完成后,关闭线程池。 通过使用ThreadPoolExecutor类,我们可以方便地创建和管理线程池,提高多线程程序的性能和效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值