在Java的线程池编程中,线程不再由用户创建,线程的创建由线程池来完成,线程池的数量也由线程池来维护,用户只需要将任务加到线程池即可,不再执行start()启动线程的操作。

Java线程池的创建(ThreadPoolExecutor)

在阿里巴巴开发手册中,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,下面介绍使用ThreadPoolExecutor线程池的方式创建多线程执行多任务。

ThreadPoolExecutor线程池

构造方法

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue)
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory) 
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              RejectedExecutionHandler handler)
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

构造方法中参数的含义

  • corePoolSize:指定了核心线程数量,代表线程池的常驻线程,创建后不会消除;
  • maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
  • keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
  • unit:keepAliveTime的时间单位,比如 TimeUnit.SECONDS
  • workQueue:任务队列,用来存放待执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
  • threadFactory:线程工厂,用于创建线程,一般用默认即可,默认new Executors.DefaultThreadFactory();
  • handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务

ThreadPoolExecutor有7个参数,其中前面5个为必传参数,后面2个为缺省参数。

线程池能够处理的最大任务是 maximumPoolSize + workQueue.size(),即任务队列数和最大线程数之和。

通过这些参数我们可以得出线程池的组成结构

线程池的组成结构

ThreadPoolExecutor拒绝策略

拒绝策略指的队列满了,还有线程进来,线程池的处理方式。

有如下4种拒绝策略。

new ThreadPoolExecutor.AbortPolicy()//队列满了,还有线程进来,不处理,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy()// 队列满了,还有线程进来,线程池不执行,调用者直接执行
new ThreadPoolExecutor.DiscardPolicy()//队列满了,丢掉任务,不抛异常
new ThreadPoolExecutor.DiscardOldestPolicy()//队列满了,尝试和最早的竞争,不抛异常

其中第一个是默认的拒绝策略 new ThreadPoolExecutor.AbortPolicy(),丢弃并抛出异常。

ThreadPoolExecutor线程创建流程

创建流程如下

线程池执行任务

当有新的任务进入线程池判断流程如下:

  1. 核心线程数:  小于核心线程池,则创建核心线程,大于则进行第2步判断。
  2. 任务队列容量  小于任务队列容量,则放入任务队列,大于则进行第3步判断。    
  3. 最大线程数量   小于最大线程数量,则创建线程,大于,则进行拒绝策略。

ThreadPoolExecutor线程创建流程

ThreadPoolExecutor创建例子

 import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExample implements Runnable{

    public String nowTime(){
        Date date = new Date();
        SimpleDateFormat dateFormat= new SimpleDateFormat("yyyy-MM-dd :hh:mm:ss");
        return dateFormat.format(date);
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程:" + Thread.currentThread().getName() + "时间:" + nowTime());

    }

    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1,//线程池的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
                3,//线程池的最大线程数量
                1,//超时时间,没人调用时就会释放
                TimeUnit.SECONDS,//超时时间的单位是秒
                new LinkedBlockingQueue<>(5),//workQueue 任务队列,被添加到线程池中,但尚未被执行的任务
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()//默认 拒绝策略,满了,不处理,抛出异常
        );
        try {
            //线程池能够处理的最大任务数是 maximumPoolSize + workQueue.size()
            //这里可创建的最大任务数是8
            for (int i = 1; i <= 8; i++) {
                System.out.println("创建任务" + i);
                threadPoolExecutor.execute(new ThreadPoolExample());
            }
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();//异常退出
        }
    }
}

输出

创建任务 1
创建任务 2
创建任务 3
创建任务 4
创建任务 5
创建任务 6
创建任务 7
创建任务 8
线程:pool-1-thread-3时间:2023-01-09 :01:48:43
线程:pool-1-thread-1时间:2023-01-09 :01:48:43
线程:pool-1-thread-2时间:2023-01-09 :01:48:43
线程:pool-1-thread-1时间:2023-01-09 :01:48:44
线程:pool-1-thread-3时间:2023-01-09 :01:48:44
线程:pool-1-thread-2时间:2023-01-09 :01:48:44
线程:pool-1-thread-3时间:2023-01-09 :01:48:45
线程:pool-1-thread-1时间:2023-01-09 :01:48:45

 threadPoolExecutor.execute(new ThreadPoolExample()); 意思是创建一个任务放到线程池。其中,线程池能够处理的最大任务数是 maximumPoolSize + workQueue.size()。

当我们创建9个线程则会抛出异常

java.util.concurrent.RejectedExecutionException: Task ThreadPoolExample @14ae5a5 rejected from java.util.concurrent.ThreadPoolExecutor@7f31245a[Running, pool size = 3, active threads = 3, queued tasks = 5, completed tasks = 0]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at ThreadPoolExample .main(ThreadPoolExample .java:42)

从运行结果可知,线程池创建了3线程pool-1-thread-1、pool-1-thread-2、pool-1-thread-3 来运行这8个任务。

在上面的例子中,根据线程池创建线程的流程,如果我们将任务数变更为小于等于5,则不会触发创建新线程的逻辑。

for (int i = 1; i <= 5; i++) {
	System.out.println("创建任务 " + i);
	threadPoolExecutor.execute(new HelloWorld());
}
此时,输出如下,
线程:pool-1-thread-1时间:2023-01-09 :02:52:08
线程:pool-1-thread-1时间:2023-01-09 :02:52:09
线程:pool-1-thread-1时间:2023-01-09 :02:52:10
线程:pool-1-thread-1时间:2023-01-09 :02:52:11
线程:pool-1-thread-1时间:2023-01-09 :02:52:12

 永远都是线程1在执行任务,显然这不是太合适,所以,我们需要根据任务数来判断缓存队列的大小设置多少合适。

总结

对于线程池的掌握,需要知道ThreadPoolExecutor线程创建的流程和7个参数的含义。