【Java 多线程】Java线程池类ThreadPoolExecutor、ScheduledThreadPoolExecutor及Executors工厂类

Java中的线程池类有两个,分别是:ThreadPoolExecutor和ScheduledThreadPoolExecutor,这两个类都继承自ExecutorService。利用这两个类,可以创建各种不同的Java线程池,为了方便我们创建线程池,Java API提供了Executors工厂类来帮助我们创建各种各样的线程池。下面我们分别介绍一下这三个类。

Java线程池ExecutorService继承树:

这里写图片描述

一、ThreadPoolExecutor

ThreadPoolExecutor是ExecutorService的一个实现类,也是java中最常用的线程池类。ThreadPoolExecutor内部维持了一个线程池,可以执行给定的任务,下面是关于它的具体使用方法。

ThreadPoolExecutor构造方法及其作用

ThreadPoolExecutor源码中的构造方法:

这里写图片描述

- corePoolSize:线程池维护线程的最少数量
- maximumPoolSize:线程池维护线程的最大数量
- keepAliveTime: 线程池维护线程所允许的空闲时间
- unit: 线程池维护线程所允许的空闲时间的单位
- workQueue: 线程池所使用的缓冲队列
- handler: 线程池对拒绝任务的处理策略
123456

线程数量控制

ThreadPoolExecutor线程池中的线程数量是可变的,其变化范围取决于下面两个变量:

1. corePoolSize:线程池维护线程的最少数量
2. maximumPoolSize:线程池维护线程的最大数量

具体线程的分配方式是,当一个任务被添加到线程池:

1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2. 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
3. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。

这样,线程池可以动态的调整池中的线程数。除了corePoolSizemaximumPoolSize两个变量外,ThreadPoolExecutor构造方法还有几个参数:

- keepAliveTime: 线程池维护线程所允许的空闲时间
- unit: 线程池维护线程所允许的空闲时间的单位
- workQueue: 线程池所使用的缓冲队列
- handler: 线程池对拒绝任务的处理策略

unit

unit 可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:

- NANOSECONDS
- MICROSECONDS
- MILLISECONDS
- SECONDS

workQueue

workQueue是一个BlockingQueue,默认是LinkedBlockingQueue<Runnable>

handler

handler 是线程池拒绝处理任务的方式,主要有四种类型:

  1. ThreadPoolExecutor.AbortPolicy()(系统默认):抛出java.util.concurrent.RejectedExecutionException异常
  2. ThreadPoolExecutor.CallerRunsPolicy():当抛出RejectedExecutionException异常时,会调用rejectedExecution方法
  3. ThreadPoolExecutor.DiscardOldestPolicy():抛弃旧的任务
  4. ThreadPoolExecutor.DiscardPolicy():抛弃当前的任务

创建一个ThreadPoolExecutor

int  corePoolSize  =    5;
int  maxPoolSize   =   10;
long keepAliveTime = 5000;

ExecutorService threadPoolExecutor =
    new ThreadPoolExecutor(
            corePoolSize,
            maxPoolSize,
            keepAliveTime,
            TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>()
            );

ThreadPoolExecutor的拒绝策略RejectedExecutionHandler

概述

RejectedExecutionHandler是一个接口:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

里面只有一个方法。当要创建的线程数量大于线程池的最大线程数的时候,新的任务就会被拒绝,就会调用这个接口里的这个方法。

可以自己实现这个接口,实现对这些超出数量的任务的处理。

ThreadPoolExecutor自己已经提供了四个拒绝策略,分别是CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy

这四个拒绝策略其实一看实现方法就知道很简单。

AbortPolicy

ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常。

private static final RejectedExecutionHandler defaultHandler =
    new AbortPolicy();12

下面是他的实现:

public static class AbortPolicy implements RejectedExecutionHandler {
    public AbortPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

很简单粗暴,直接抛出个RejectedExecutionException异常,也不执行这个任务了。

测试

先自定义一个Runnable,给每个线程起个名字,下面都用这个Runnable

static class MyThread implements Runnable {
        String name;
        public MyThread(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程:"+Thread.currentThread().getName() +" 执行:"+name +"  run");
        }
    }

然后构造一个核心线程是1,最大线程数是2的线程池。拒绝策略是AbortPolicy

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 0, 
        TimeUnit.MICROSECONDS, 
        new LinkedBlockingDeque<Runnable>(2), 
        new ThreadPoolExecutor.AbortPolicy());1234
for (int i = 0; i < 6; i++) {
    System.out.println("添加第"+i+"个任务");
    executor.execute(new MyThread("线程"+i));
    Iterator iterator = executor.getQueue().iterator();
    while (iterator.hasNext()){
        MyThread thread = (MyThread) iterator.next();
        System.out.println("列表:"+thread.name);
    }
}

输出是:

这里写图片描述

分析一下过程。

  1. 添加第一个任务时,直接执行,任务列表为空。
  2. 添加第二个任务时,因为采用的LinkedBlockingDeque,,并且核心线程正在执行任务,所以会将第二个任务放在队列中,队列中有 线程2.
  3. 添加第三个任务时,也一样会放在队列中,队列中有 线程2,线程3.
  4. 添加第四个任务时,因为核心任务还在运行,而且任务队列已经满了,所以胡直接创建新线程执行第四个任务,。这时线程池中一共就有两个线程在运行了,达到了最大线程数。任务队列中还是有线程2, 线程3.
  5. 添加第五个任务时,再也没有地方能存放和执行这个任务了,就会被线程池拒绝添加,执行拒绝策略的rejectedExecution方法,这里就是执行AbortPolicy的rejectedExecution方法直接抛出异常。
  6. 最终,只有四个线程能完成运行。后面的都被拒绝了。

CallerRunsPolicy

CallerRunsPolicy在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。

下面说他的实现:

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public CallerRunsPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

也很简单,直接run。

测试

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 30,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<Runnable>(2),
        new ThreadPoolExecutor.AbortPolicy());1234

按上面的运行,输出

这里写图片描述

注意在添加第五个任务,任务5 的时候,同样被线程池拒绝了,因此执行了CallerRunsPolicy的rejectedExecution方法,这个方法直接执行任务的run方法。因此可以看到任务5是在main线程中执行的。

从中也可以看出,因为第五个任务在主线程中运行,所以主线程就被阻塞了,以至于当第五个任务执行完,添加第六个任务时,前面两个任务已经执行完了,有了空闲线程,因此线程6又可以添加到线程池中执行了。

这个策略的缺点就是可能会阻塞主线程。

DiscardPolicy

这个策略的处理就更简单了,看一下实现就明白了:

public static class DiscardPolicy implements RejectedExecutionHandler {
    public DiscardPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}

这个东西什么都没干。

因此采用这个拒绝策略,会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行。

测试

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 30,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<Runnable>(2),
        new ThreadPoolExecutor.DiscardPolicy());

输出:

这里写图片描述

可以看到 后面添加的任务5和6根本不会执行,什么反应都没有,直接丢弃。

DiscardOldestPolicy

DiscardOldestPolicy策略的作用是,当任务呗拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public DiscardOldestPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}

在rejectedExecution先从任务队列总弹出最先加入的任务,空出一个位置,然后再次执行execute方法把任务加入队列。

测试

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 30,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<Runnable>(2),
        new ThreadPoolExecutor.DiscardOldestPolicy());1234

输出是:

这里写图片描述

可以看到,

  1. 在添加第五个任务时,会被线程池拒绝。这时任务队列中有 任务2,任务3
  2. 这时,拒绝策略会让任务队列中最先加入的任务弹出,也就是任务2.
  3. 然后把被拒绝的任务5添加人任务队列,这时任务队列中就成了 任务3,任务5.
  4. 添加第六个任务时会因为同样的过程,将队列中的任务3抛弃,把任务6加进去,任务队列中就成了 任务5,任务6
  5. 因此,最终能被执行的任务只有1,4,5,6. 任务2和任务3倍抛弃了,不会执行。

自定义拒绝策略

通过看前面的系统提供的四种拒绝策略可以看出,拒绝策略的实现都非常简单。自己写亦一样

比如现在想让被拒绝的任务在一个新的线程中执行,可以这样写:

static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        new Thread(r,"新线程"+new Random().nextInt(10)).start();
    }
}

然后正常使用:

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 30,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<Runnable>(2),
        new MyRejectedExecutionHandler());1234

输出:

这里写图片描述

发现被拒绝的任务5和任务6都在新线程中执行了。

img

二、ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor是ExecutorService的另一个实现类,从上面Java线程池ExecutorService继承树这幅图可以看出,ScheduledThreadPoolExecutor直接继承自ScheduledExecutorService,ScheduledThreadPoolExecutor 类的功能也主要体现在ScheduledExecutorService 接口上,而所以在介绍ScheduledThreadPoolExecutor之前先介绍一下ScheduledExecutorService接口。

2.1 ScheduledExecutorService接口介绍

java.util.concurrent.ScheduledExecutorService接口继承了ExecutorService,它的最主要的功能就是可以对其中的任务进行调度,比如延迟执行、定时执行等等。

ScheduledExecutorService接口定义:

public interface ScheduledExecutorService extends ExecutorService {

    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);


    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay, long period, TimeUnit unit);

    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay, long delay,TimeUnit unit);

}

从上面接口定义我们知道,提供了四个方法,下面我们就分别介绍:

1. schedule (Runnable task, long delay, TimeUnit timeunit)
2. schedule (Callable task, long delay, TimeUnit timeunit)
3. scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
4. scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)

2.1.1 schedule (Runnable task, long delay, TimeUnit timeunit)

这个方法的意思是在指定延迟之后运行task。这个方法有个问题,就是没有办法获知task的执行结果。如果我们想获得task的执行结果,我们可以传入一个Callable的实例(后面会介绍)。

ScheduledExecutorService scheduledExecutorService =
    Executors.newScheduledThreadPool(5);

ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
    public Object call() throws Exception {
        System.out.println("Executed!");
        return "Called!";
    }
},5,TimeUnit.SECONDS);
System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown();
1234567891011121314

2.1.2 schedule (Callable task, long delay, TimeUnit timeunit)

这个方法与schedule (Runnable task)类似,也是在指定延迟之后运行task,不过它接收的是一个Callable实例,此方法会返回一个ScheduleFuture对象,通过ScheduleFuture我们可以取消一个未执行的task,也可以获得这个task的执行结果。

ScheduledExecutorService scheduledExecutorService =
    Executors.newScheduledThreadPool(5);

ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
    public Object call() throws Exception {
        System.out.println("Executed!");
        return "Called!";
    }
},
5,
TimeUnit.SECONDS);

System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown();
123456789101112131415

2.1.3 scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)

这个方法的作用是周期性的调度task执行。task第一次执行的延迟根据initialDelay参数确定,以后每一次执行都间隔period时长。

如果task的执行时间大于定义的period,那么下一个线程将在当前线程完成之后再执行。整个调度保证不会出现一个以上任务同时执行。

2.1.4 scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)

scheduleWithFixedDelay的参数和scheduleAtFixedRate参数完全一致,它们的不同之处在于对period调度周期的解释。

在scheduleAtFixedRate中,period指的两个任务开始执行的时间间隔,也就是当前任务的开始执行时间和下个任务的开始执行时间之间的间隔。

而在scheduleWithFixedDelay中,period指的当前任务的结束执行时间到下个任务的开始执行时间。

2.1.5 ScheduledExecutorService的关闭

和ExecutorService类似, 我们在使用完ScheduledExecutorService时需要关闭它。如果不关闭的话,JVM会一直运行直,即使所有线程已经关闭了。

关闭ScheduledExecutorService可以使用其继承自ExecutorService接口的shutdown()shutdownNow()方法,两者的区别请参考【Java线程池 ExecutorService】。

2.2 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,构造参数很简单,只有3个:

1. int corePoolSize:线程池维护线程的最少数量
2. ThreadFactory threadFactory:线程工程类,线程池用它来制造线程
3. RejectedExecutionHandler handler:线程池对拒绝任务的处理策略

具体使用方法请参考ThreadPoolExecutor或者使用Executors。

三、Executors

创建一个什么样的ExecutorService的实例(即线程池)需要我们的具体应用场景而定,不过Java给我们提供了一个Executors工厂类,它可以帮助我们很方便的创建各种类型ExecutorService线程池,Executors一共可以创建下面这四类线程池:

1. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

备注:Executors只是一个工厂类,它所有的方法返回的都是ThreadPoolExecutor、ScheduledThreadPoolExecutor这两个类的实例

posted @ 2021-01-14 19:59  satire  阅读(157)  评论(0编辑  收藏  举报