Hey, Nice to meet You. 

必有过人之节.人情有所不能忍者,匹夫见辱,拔剑而起,挺身而斗,此不足为勇也,天下有大勇者,猝然临之而不惊,无故加之而不怒.此其所挟持者甚大,而其志甚远也.          ☆☆☆所谓豪杰之士,

Callable、Future和线程池(ThreadPoolExecutor)的基础学习

本文介绍另外两种创建多线程的方式,这两种方式我们在实际中会用的多一点,尤其是线程池。而在前面文章中我们讲述了创建线程最基本的两种方式:一种是直接继承Thread,另外一种就是实现Runnable接口。但是这两种方式创建线程有一个缺陷,那就是无法获取到线程运行后的结果,因为这两个方式都是重写了 run()方法,而run()方法是用void修饰的。所以后来就有了Callable和Future这两个接口,它们能够获取线程执行的结果。

1、Callable介绍

Callable是在JDK1.5中出现的接口,它和Runnable接口很相似,所以可以认为:Callable接口是Runnable接口的增强版,因为Runnable有的功能Callable都有,而且还能获取任务执行的结果。所以下面来看一下Callable和Runnable接口的对比:

先来看一下Runnable接口的源码:

public interface Runnable {
    public abstract void run();
}

Callable接口的源代码:

public interface Callable<V> {
    V call() throws Exception;
}

可以很明显的看出它们二者的区别:

  1. Callable使用的是call(),而Runnable中使用的是run()。
  2. Callable的call()可以抛出异常,而Runnable的run()不会抛出异常。
  3. Callable能接受一个泛型,然后在call()中返回一个这个类型的值。而Runnable的run()没有返回值。
  4. 补充:Callable不能直接替换Runnable,因为Thread类的构造方法根本没有Callable。

上面说Callable是可以返回任务执行结果的,而获取返回结果需使用到Future。所以下面要介绍一下Future。

2、Future介绍

Future也是一个接口,通过它可以获得任务执行的返回值。该接口的内部源码如下:

public interface Future<V> {
  boolean cancel(boolean mayInterruptIfRunning);

  boolean isCancelled();

  boolean isDone();

  V get() throws InterruptedException, ExecutionException;

  V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

可以发现在Future接口中声明了5个方法,下面依次解释每个方法的作用:

  • cancel(boolean mayInterruptIfRunning):用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  • isCancelled():表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
  • isDone():表示任务是否已经完成,若任务完成,则返回true;
  • get():用来获取执行结果,这个方法会产生阻塞,阻塞的线程为调用get()方法的线程,会一直等到任务执行完毕返回结果,之后阻塞的主线程才能够往后执行。
  • get(long timeout, TimeUnit unit):用来获取执行结果,如果在指定时间内,还没获取到结果,就会抛出TimeoutException异常(慎用这个方法,因为有很多坑)。

下面是Future接口中在java.util.concurrent包下类的结构图:

image

由于Future只是一个接口,所以是无法直接用来创建对象使用的,所以真正获取结果用到的是FutureTask这个类。


『FutureTask』

通过上面的图片发现FutureTask类是实现了RunnableFuture接口,而这个接口又继承了Future接口,我们具体点开其源码来看。

public class FutureTask<V> implements RunnableFuture<V>{
    code...
}

打开RunnableFuture接口的实现:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

可以看出RunnableFuture继承了Runnable和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。


上面说了这么多,接下来使用Callable+FutureTask创建线程并获取执行结果的一个栗子如下:

  1. 创建一个实现Callable接口的类。
  2. 重写call方法,将线程要执行的操作定义在call()中。
  3. 创建Callable接口实现类的对象。
  4. 创建FutureTask对象,并将上面Callable接口实现类的对象传入FutureTask构造器中。
  5. 将FutureTask的对象作为参数传入Thread类的构造器中,创建Thread类对象,并且启动线程。
  6. 获取Callable中call方法的返回值。
package com.thr;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author Administrator
 * @date 2020-04-09
 * @desc Callable+Future创建并获取线程执行结果
 */
//1、创建一个实现Callable接口的类
class MyCallable implements Callable<Integer>{

    //2、重写call方法,将线程要执行的操作定义在call()中
    @Override
    public Integer call() throws Exception {
        int num=0;
        for (int i = 1; i <= 100; i++) {
            num+=i;
        }
        return num;
    }
}

public class CallableFutureDemo {

    public static void main(String[] args) {
        //3、创建Callable接口实现类的对象
        MyCallable callable = new MyCallable();
        //4、创建FutureTask对象,并将上面Callable接口实现类的对象传入FutureTask构造器中
        FutureTask<Integer> task = new FutureTask<Integer>(callable);
        //5、将FutureTask的对象作为参数传入Thread类的构造器中,创建Thread类对象,并且启动线程
        new Thread(task).start();
        try {
            //6、获取Callable中call方法的返回值,调用get()时,main线程会阻塞,知道任务线程返回结果
            Integer integer = task.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

关于FutureTask的一些小结:

  • 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成。
  • 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
  • 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
  • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。
  • 一旦计算完成,就不能再重新开始或取消计算。
  • get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
  • get()只会计算一次,并且会导致主线程阻塞,所以get()方法一般发在最后。

我们知道Callable用于产生结果,Future用于获取结果。不过Callable和Future一般都和线程池搭配使用,所以下面再来简单介绍一下线程池的使用。

3、线程池的介绍

在前面的文章中介绍了Thread、Runnable和Callable这三种方式创建线程,我们在创建少量线程的时候使用它们是非常的简单方便的,但是如果我们需要创建成百上千的线程时,那么岂不是要创建成百上千个线程对象,调用成百上千的start()方法,可见这样是非常浪费时间、消耗资源和降低程序效率的,因为线程的创建和销毁需要耗费大量的系统资源。那为了解决这一问题出现了线程池。


线程池顾名思义,就是由很多线程构成的池子。在有任务的时候随时取用线程,当任务完成后又将线程放回池中。

所以合理利用线程池能够带来三个好处。

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

线程池的创建一般用Executors这个工具类来创建,常见的有以下四种方式:

  • newFixedThreadPool(int nThreads):创建一个固定线程数目的线程池,超出的线程处理数量的任务会在队列中等待。
  • newSingleThreadExecutor():创建一个单线程化的线程池。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • newCacheThreadPool():创建一个可缓存的线程池。如果现有任务没有线程来处理,则创建一个新线程并添加到缓存池中。如果有被使用完但是还没销毁的线程,就复用该线程。如果有线程60s未被使用的话就会从缓存中移出并终止(销毁)。因此,长时间保持空闲的线程池不会使用任何资源。
  • newScheduledThreadPool(int corePoolSize):创建一个大小无限的支持定时及周期性的任务执行的线程池,多数情况下可用来替代Time类。

『一般不推荐使用Executors的方式来创建线程池,因为可能会出现OOM(Out Of Memory,内存溢出)的情况,下面我们依次详细的分析这四个方式:』


①、Executors.newFixedThreadPool(int nThread)

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

可以发现最后一行使用了LinkedBlockingQueue,泛型是Runnable类型,这里的队列是用来存放线程任务的。我们再来看看这个LinkedBlockingQueue部分源码:

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
}

public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
}

在上一章博客中提过LinkedBlockingQueue是链表实现的有界阻塞队列,其capacity是可以选择进行设置的,如果不设置的话,将是一个无边界的阻塞队列,队列的最大长度为Integer.MAX_VALUE。而上面newFixedThreadPool的源码中,我们可以很清晰的看到LinkedBlockingQueue是没有指定capacity的。所以此时LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为队列中等待的线程数太多而导致OOM。

下面我们来一个简单的例子,模拟一下使用Executors导致OOM的情况:

首先将JVM参数调一下:-Xmx8m –Xms8m

image

示例代码如下:

package com.thr;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author Administrator
 * @date 2020-04-11
 * @desc Excutors出现OOM举例
 */
public class ExecutorsDemo {
    private static ExecutorService service = Executors.newFixedThreadPool(15);

    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            service.execute(new SubThread());
        }
    }
}

class SubThread implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            //do nothing
        }
    }
}

运行结果:

image


②、Executors.newSingleThreadExexutor()

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

可以发现还是使用阻塞队列LinkedBlockingQueue,所以问题是一样的。


③、Executors.newCacheThreadPool()

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}

可以发现ThreadPoolExecutor对象中的第二个参数为Integer.MAX_VALUE,而这个位置参数的意思为线程池最大线程数。所以还是会出现OOM的情况。


④、Executors.newScheduleThreadPool()

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
}

public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {

        code...

        }

通过上面三段代码可以发现newScheduleThreadPool()方法返回了ScheduledThreadPoolExecutor对象,而它又继承了ThreadPoolExecutor类,并且调用的是父类的构造器,而构造器中的第二个参数为Integer.MAX_VALUE,所以还是同样的问题。

newScheduleThreadPool()的简单案例(由于前面三个的使用非常的简单,所有就不举例了) 代码如下:

[1]、延迟执行,线程在延迟5秒后执行。

/**
 * newScheduleThreadPool()的简单案例
 */
public class ExecutorDemo {
    public static void main(String[] args) {

        // 创建一个定时处理任务的线程池
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);

        // 设置延迟5秒执行,时间到了就会执行线程
        executorService.schedule(() -> {
            System.out.println("newScheduledThreadPool---延迟5秒后打印");
        }, 5, TimeUnit.SECONDS);

        // 释放资源
        executorService.shutdown();
    }
}

[2]、定期执行,表示延迟5秒后每3秒执行一次。特别注意:线程池不能关闭

/**
 * newScheduleThreadPool()的简单案例
 */
public class ExecutorDemo {
    public static void main(String[] args) {

        // 创建一个定时处理任务的线程池
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);

        // 设置延迟5秒,然后没过3秒打印一次
        executorService.scheduleAtFixedRate(() -> {
            System.out.println("newScheduledThreadPool---延迟5秒后每过3秒打印一次");
        }, 5,3, TimeUnit.SECONDS);

        // 注意这里不能释放资源
        //executorService.shutdown();
    }
}

这就是使用Executors工具类创建线程池的缺陷所在,在《阿里巴巴开发手册》中是不建议使用这种方式创建线程池的,而是推荐使用new ThreadPoolExecutor构造函数来创建线程池。如果你细心一点会发现上面四种方式中其实最终都是使用ThreadPoolExecutor这个类,所以这个类才是线程池的核心,我们只有彻底了解这个类才能真正的理解线程池。

image

4、ThreadPoolExecutor

上面既然说推荐使用ThreadPoolExecutor来创建线程池,那么先来看一下ThreadPoolExecutor的内容。在ThreadPoolExecutor类中提供了四个构造器,由于前三个构造器其实都是调用了第四个构造器来完成初始化的,所以这里就列出第四个构造器:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

可以发现构造器有7个参数,这7个参数特别的重要,下面分别解释下构造器中各个参数的含义:

  • corePoolSize:核心池的大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。所以在默认情况下,创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中,例如LinkedBlockingQueue,如果缓存队列中存放的任务满了,则会继续创建新的线程来执行任务,直到创建maximumPoolSize个线程为止,也就是下面要定义的参数。
  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程,所以它的数值肯定是不能小于corePoolSize的,从源码中也可以看到,如果这样做在运行时会抛出异常:IllegalArgumentException。
  • keepAliveTime:空闲线程的存活时间。就是当线程的数量大于corePoolSize时,如果等待了keepAliveTime时长还没有任务可执行,则线程终止(前提是线程池中的线程数必须大于corePoolSize时,keepAliveTime才会起作用,否则是没用的),直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。
  • unit:它为参数keepAliveTime的时间单位,它在TimeUnit类中有7种静态属性可取。
    • 天:TimeUnit.DAYS;
    • 小时:TimeUnit.HOURS;
    • 分钟:TimeUnit.MINUTES;
    • 秒:TimeUnit.SECONDS;
    • 毫秒:TimeUnit.MILLISECONDS;
    • 微妙:TimeUnit.MICROSECONDS;
    • 纳秒:TimeUnit.NANOSECONDS;
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序,可以指定缓存队列的大小。
    • LinkedBlockingQueue:一个基于链表结构的无界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序,吞吐量通常要高于ArrayBlockingQueue,所以这个比较常用,静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:它没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素,否则插入操作一直处于阻塞状态。拥有公平(FIFO)和非公平(LIFO,SynchronousQueue默认)两种策略模式,非公平模式很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。但是它的吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定)。但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
    • 注:其中ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和SynchronousQueue。
  • threadFactory:线程工厂,它用于创建新的线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
  • handler:线程饱和策略或拒绝策略。当线程池和队列都满了,再加入的任务会执行此策略,它有四种策略。
    • AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。线程池默认的拒绝策略。如何使用new ThreadPoolExecutor.AbortPolicy()
    • DiscardPolicy:也是丢弃任务,但是不抛出异常。
    • DiscardOldestPolicy:丢弃队列最前面的任务,也就是队列头的元素,然后重新尝试执行任务(重复此过程)。如果此时阻塞队列使用PriorityBlockingQueue优先级队列,将会导致优先级最高的任务被抛弃。
    • CallerRunsPolicy:既不抛弃任务也不抛出异常,而是由调用线程的主线程来处理该任务。换言之就是由调用线程池的主线程自己来执行任务(例如:是有main线程启动的线程池,当触发次策略时,多余的任务就会交由main线程来执行),因此在执行任务的这段时间里主线程无法再提交新任务,从而使线程池中工作线程有时间将正在处理的任务处理完成,所以对性能和效率必然是极大的损耗。

上面既然介绍完了线程池构造方法中的各个参数,那么再来介绍线程池的工作流程:

image

  • 当线程池中的线程个数小于corePoolSize,每次提交一个新任务到线程池时,都会创建一个新的工作线程来执行任务,直到当前线程数等于corePoolSize;
  • 当线程池中的线程个数等于corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
  • 当线程池中的线程个数等于corePoolSize时,并且队列也满了(有界队列),这个时候再来新的任务,就会继续创建新的线程去处理(非核心线程),直到线程池中的线程数达到maximumPoolSize。这个时候创建的线程,当线程空闲下来的时候经过keepAliveTime的时间就会被销毁;
  • 当线程池中的线程数达到maximumPoolSize时,这个时候再来新的任务,就由饱和策略来处理提交的任务;

注:如果存储任务的队列满了,并且线程数量大于corePoolSize,小于maximumPoolSize,此时创建的线程会优先执行新来的任务,之后在执行队列中的。


我们在打开ThreadPoolExecutor类的代码可以看到,ThreadPoolExecutor继承了AbstractExecutorService,我们来看一下AbstractExecutorService的实现:

public abstract class AbstractExecutorService implements ExecutorService {
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };
    public Future<?> submit(Runnable task) {};
    public <T> Future<T> submit(Runnable task, T result) { };
    public <T> Future<T> submit(Callable<T> task) { };
    private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
    };
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException {
    };
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
    };
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
    };
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
        throws InterruptedException {
    };
}

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。我们接着看ExecutorService接口的实现:

public interface ExecutorService extends Executor {
    void shutdown();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

而ExecutorService又是继承了Executor接口,我们看一下Executor接口的实现:

public interface Executor {
    void execute(Runnable command);
}

所以到这里,大家应该明白了ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor几个之间的关系了。

image

Executor是一个线程池顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,它就是用来执行传进去的任务的,但没有返回值;

然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutdown等;

抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;

然后ThreadPoolExecutor继承了类AbstractExecutorService。

在ThreadPoolExecutor类中有几个非常重要的方法:

  • execute(Runnable command):用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
  • submit(Callable task)/submit(Runnable task):用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit),在指定的时间内会等待任务执行,超时则抛出超时异常,等待时候会阻塞当前线程。
  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
  • awaitTermination(long timeout, TimeUnit unit):用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

所以通过上面的讲解大家应该知道创建线程池的正确姿势了吧:

ExecutorService es = new ThreadPoolExecutor(5,,20,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(10));

最后简单的举个创建线程池的例子吧:

package com.thr;

import java.util.concurrent.*;
/**
 * @author Administrator
 * @date 2020-04-11
 * @desc 使用自定义参数ThreadPoolExecutor创建线程池
 */
public class ExecutorServiceDemo {
    public static void main(String[] args) {
    //定义线程池参数
        ExecutorService es = new ThreadPoolExecutor(5, 20,0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10));
        //创建Callable和Future对象
        MyCallable myCallable = new MyCallable();
        Future<Integer> future = es.submit(myCallable);
        try {
            //获取结果并打印
            Integer num = future.get();
            System.out.println(num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            //关闭线程池
            es.shutdown();
        }
    }
}

class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i = 1; i <= 100; i++) {
            sum+=i;
        }
        return sum;
    }
}

当然除了自己定义ThreadPoolExecutor外。还有其他方法。比如各种开源工具如Guava等。这里推荐使用Guava提供的ThreadFactoryBuilder来创建线程池。因为当我们需要给新创建的线程取名字、或者设置为守护线程、错误处理器等操作时,它的好处就体现出来了。简单举例:(注意使用Guava需要引入包)

public class ThreadFactoryBuilderTest {

    public static void main(String[] args) {
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("线程名称-%s").build();
        // 创建一个线程对象
        Thread newThread = threadFactory.newThread(()->{

        });
        System.out.println(newThread.getName());
    }
}

参考资料:

posted @ 2020-04-12 18:31  唐浩荣  阅读(2127)  评论(0编辑  收藏  举报