线程池的使用(结合Future/Callable使用)

概述

线程池的创建⽅法总共有 7 种(其中 6 种是通过 Executors 创建的, 1 种是通过ThreadPoolExecutor 创建的),但总体来说可分为 2 类:

  1. 通过 ThreadPoolExecutor 创建的线程池;
  2. 通过 Executors 创建的线程池(下面只说四种)。

7种创建方法
java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。

Executor/ExecutorService

Executor,Executors,ExecutorService,ThreadExecutorPool之间的关系,博文
线程池的使用(结合Future/Callable使用)_第1张图片

常使用的Executors实现创建线程池使用线程主要是用下面类图中提供的类,其线程池类图如下:
线程池的使用(结合Future/Callable使用)_第2张图片
它包含了三个executor接口:

  1. Executor:运行新任务的简单接口
  2. ExecutorService:扩展了Executor,添加了用来管理执行器生命周期和任务生命周期的方法
  3. ScheduleExcutorService:扩展了ExecutorService,支持Future和定期执行任务

参考博文

Executors

是一个线程池工厂,提供了很多的工厂方法,创建了下面的线程池

// 创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor();
// 创建固定数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads);
// 创建带缓存的线程池
public static ExecutorService newCachedThreadPool();
// 创建定时调度的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 创建流式(fork-join)线程池,创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】
public static ExecutorService newWorkStealingPool();
//创建⼀个单线程的可以执⾏延迟任务的线程池;
Executors.newSingleThreadScheduledExecutor

java通过Executors提供四种线程池:自动创建线程池的几种方式都封装在Executors工具类中

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

线程池核心类-ThreadPoolExecutor

ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置;

这个用于手动创建线程池,供了好几个构造方法,最底层的构造方法只有下面这一个

/*
	corePoolSize:核心线程数,
		也是线程池中常驻的线程数,
		线程池初始化时默认是没有线程的,
		当任务来临时才开始创建线程去执行任务
	
	maximumPoolSize:最大线程数,
		在核心线程数的基础上可能会额外增加一些非核心线程,
		需要注意:只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
	
	keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,
		注意当corePoolSize=maxPoolSize时,
		keepAliveTime参数也就不起作用了(因为不存在非核心线程);
	
	unit:keepAliveTime的时间单位
	
	workQueue:用于保存任务的队列,
		等待队列,线程池中的线程数超过核心线程数时,任务将放在等待队列,
		它是一个BlockingQueue类型的对象
		可以为无界、有界、同步移交三种队列类型之一,
		当池子里的工作线程数大于corePoolSize时,
		这时新进来的任务会被放到队列中
	
	threadFactory:创建线程的工厂类,
		默认使用Executors.defaultThreadFactory(),
		也可以使用guava库的ThreadFactoryBuilder来创建
	
	handler:线程池无法继续接收任务
		队列已满且线程数达到maximunPoolSize)时的饱和策略
		取值有:
			AbortPolicy:中断抛出异常
			CallerRunsPolicy:默默丢弃任务,不进行任何通知
			DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
			DiscardPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)

*/
 public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {……}

对于上面的这些参数,最关心的是workQueue、threadFactory和handler

等待队列-workQueue

等待队列是BlockingQueue类型的,理论上只要是它的子类,我们都可以用来作为等待队列。
下面是jdk内部自带一些阻塞队列

  1. ArrayBlockingQueue,队列是有界的,基于数组实现的阻塞队列
  2. LinkedBlockingQueue,队列可以有界,也可以无界。基于链表实现的阻塞队列
  3. SynchronousQueue,不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作将一直处于阻塞状态。该队列也
  4. Executors.newCachedThreadPool()的默认队列
  5. PriorityBlockingQueue,带优先级的无界阻塞队列

线程工厂-threadFactory

ThreadFactory是一个接口,只有一个方法

拒绝策略-handler

拒绝策略,就是当线程池满了、队列也满了的时候,我们对任务采取的措施

  1. AbortPolicy:中断抛出异常
  2. CallerRunsPolicy:默默丢弃任务,不进行任何通知
  3. DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
  4. DiscardPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)

四种策略各有优劣,比较常用的是DiscardPolicy,但是这种策略有一个弊端就是任务执行的轨迹不会被记录下来。所以,我们往往需要实现自定义的拒绝策略, 通过实现RejectedExecutionHandler接口的方式

FutureTask/Runnable

FutureTask一个可取消的异步计算,FutureTask 实现了Future的基本方法
线程池的使用(结合Future/Callable使用)_第3张图片
FutureTask类中常用方法:博客参考2

  1. public boolean isCancelled() 如果此任务在正常完成之前取消,则返回 true 。
  2. public boolean isDone() 返回true如果任务已完成。
  3. public V get() 等待计算完成,然后检索其结果。
  4. public V get(long timeout, TimeUnit unit),如果需要等待最多在给定的时间计算完成,然后检索其结果(如果可用)。
  5. public boolean cancel(boolean mayInterruptIfRunning),尝试取消执行此任务。
  6. protected void set(V v):将此未来的结果设置为给定值,除非此未来已被设置或已被取消。

创建FutureTask的两种方法:博客参考1

  1. 使用Callable
     FutureTask<Boolean> future = new FutureTask<>(new Callable<Boolean>() {
       @Override
       public Boolean call() throws Exception {
         return true;
       }
     });
    
    
  2. 直接new一个FutureTask
    //托管给线程池处理
    Future<?> futureResult = Executors.newCachedThreadPool().submit(future);
    //托管给单独线程处理
    //FutureTask继承了Runnable接口,
    //所以它可以通过new Thread()的方式进行运行,
    //再由future变量来检索结果值或者取消任务等操作,通过线程池托管的方式也可以适用。
    new Thread(future).start();
    
    

Callable

线程池

参考博文

newCachedThreadPool

特点:

  1. 线程池数量上限为:Integer.MaxValue(2147483647);
  2. 线程池默认空闲60S,超过60S会从线程池中移除
  3. 新来任务时,先检查是否有空闲线程可使用,若无,则创建一个新线程执行任务
  4. 任务存储在同步队列中。
  5. 适用场景:短时异步任务
    构造方法:
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

newFixedThreadPool

特点:

  1. 用于创建一个可重用、固定线程数量的线程池;
  2. 当线程池中线程都处于运行中时,新来的线程会进入等待状态,直到线程池中出现一个空闲线程
  3. 当一个线程在任务中途退出、终止时,会有一个新的线程来替代它继续完成后面未完成的任务。
  4. 除非采用显式关闭的方法去关闭某个线程,否则线程会一直存在,不会释放资源。
  5. 任务存储在无界阻塞队列中
  6. 适用场景:长期任务

构造方法:

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

newScheduledThreadPool

特点:

  1. 任务存储在无界延迟队列中
  2. 适用场景:需要定期执行或延迟执行的任务

构造方法:

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

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

newSingleThreadExecutor

特点:

  1. 创建一个单个Worker的线程;
  2. 线程会按照顺序依次执行
  3. 任务存储在无界阻塞队列中
  4. 适用场景:需要按照顺序执行的任务

构造方法:

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

常用方法:

public static void main(String args[]) 
{
    /*
     * 执行定时任务newScheduledThreadPool
     */
    ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
    //方法1:5秒后开始执行,每隔一秒执行一次
    /*
		scheduleAtFixedRate方法,一共四个参数,分别是:
			1.需要执行的任务task、
			2.延迟执行时间t1、
			3.每次执行任务的时间间隔t2、
			4.时间间隔单位。
		含义是:在t1时间过后,以 1次/t2 的频率来不断执行 task。
		代码中,在5秒延迟后,以 1次/1秒的频率执行 打印当前时间的任务。
	*/
    service.scheduleAtFixedRate(new ExecutorsTest2(), 5, 1, TimeUnit.SECONDS);
   
	
	//方法2:5秒后开始执行,每次任务执行完后延迟3秒后,继续执行下一次
	/*
		scheduleWithFixedDelay也是四个参数,分别是:
			1.待执行的任务Task,
			2.延迟时间t1,
			3.每次任务执行完毕后延迟t2秒后执行下次任务,
			4.延迟时间单位。
	*/
    service.scheduleWithFixedDelay(new ExecutorsTest3(), 5, 3, TimeUnit.SECONDS);
}

配置线程池的参数

根据任务的特性来分析。

  1. 任务的性质:CPU密集型、IO密集型和混杂型
  2. 任务的优先级:高中低
  3. 任务执行的时间:长中短
  4. 任务的依赖性:是否依赖数据库或者其他系统资源

如果任务属于CPU密集型,那么我们可以将线程池数量设置成CPU的个数,以减少线程切换带来的开销。
如果任务属于IO密集型,我们可以将线程池数量设置得更多一些,比如CPU个数*2。

可以通过Runtime.getRuntime().availableProcessors()来获取CPU的个数

与Future/Callable联合使用

主线程需要知道子线程的运行结果,以便确定如何执行任务.JDK1.5以后就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
使用Callable步骤:参考博文

  1. 任务类实现Callable接口
  2. 创建线程池:ExecutorService es = Executors.newCachedThreadPool();
  3. 执行任务:chuju cj = new chuju();Future future = es.submit(cj);
  4. 获取子线程中任务的执行结果:future.get()

Future

FutureTask 类是 Future 接口的实现类,提供对异步任务的操作的具体实现。

FutureTask 类不仅仅实现了 Future 接口,而且实现了 Runnable 接口,或者更加准确地说,FutureTask 类实现 了 RunnableFuture 接口

参考博客
线程池的使用(结合Future/Callable使用)_第4张图片

当我们执行某一耗时的任务时,我们可以另起一个线程异步去执行这个耗时的任务,同时我们可以干点其他事情。当事情干完后我们再根据future这个"单号"去提取耗时任务的执行结果即可。因此Future也是多线程中的一种应用模式

跟Future使用有关的JDK类主要有FutureTask和Callable两个

把任务提交给子线程去处理,主线程不用同步等待,当向线程池提交了一个Callable或Runnable任务时就会返回Future,用Future可以获取任务执行的返回结果

Future接口象征着异步执行任务的结果即执行一个耗时任务完全可以另起一个线程执行,然后此时我们可以去做其他事情,做完其他事情我们再调用Future.get()方法获取结果即可,此时若异步任务还没结束,此时会一直阻塞等待,直到异步任务执行完获取到结果。

Future的主要方法包括:

  1. get()方法:返回任务的执行结果,若任务还未执行完,则会一直阻塞直到完成为止,如果执行过程中发生异常,则抛出异常,但是主线程是感知不到并且不受影响的,除非调用get()方法进行获取结果则会抛出ExecutionException异常;

  2. get(long timeout, TimeUnit unit):在指定时间内返回任务的执行结果,超时未返回会抛出TimeoutException,这个时候需要显式的取消任务;

  3. cancel(boolean mayInterruptIfRunning):取消任务,boolean类型入参表示如果任务正在运行中是否强制中断;

  4. isDone():判断任务是否执行完毕,执行完毕不代表任务一定成功执行,比如任务执行失但也执行完毕、任务被中断了也执行完毕都会返回true,它仅仅表示一种状态说后面任务不会再执行了;

  5. isCancelled():判断任务是否被取消;

参考博文

参考2

Callable

Runnable是出自jdk1.0,Callable出自jdk1.5,所以Callable肯定对于前者有增强

Runnable的run方法与Callable的call方法进行对比:Callable的call方法有返回值并可以抛出异常,而run方法没有返回值

// Runnable
public abstract void run();

// Callable
V call() throws Exception;

Callable的基本用法

通过FutureTask,交给线程池

通过FutureTask,交给线程池执行,阻塞等待结果返回

  1. 直接使用submit:callable的逻辑是在异步线程中处理的,主线程通过阻塞式接受异步线程返回的内容。简单来说,Callable里面执行的内容是在另一个线程中执行,但是这个线程是阻塞的,需要执行完,才能继续主线程的运行

        public static void main(String[] args) throws Exception {
            FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
                public String call() throws InterruptedException {
                    Thread.sleep(3000L);
                    System.out.println("当前线程:" + Thread.currentThread().getName());
                    return "hello world";
                }
            });
            ExecutorService executorService = Executors.newFixedThreadPool(1);
            System.out.println("开始时间戳为:" + System.currentTimeMillis());
            executorService.submit(futureTask);
            String result = futureTask.get();
            System.out.println("结束时间戳为:" + System.currentTimeMillis() + ",result = " + result);
        }
    结果打印:
    开始时间戳为:...17192
    当前线程:pool-1-thread-1
    结束时间戳为:...20202,result = hello world
    
  2. 直接使用FutureTask.run():和直接调用Runnable的方法一样,一直都是由主线程执行

        public static void main(String[] args) throws Exception {
            FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
                public String call() throws InterruptedException {
                    Thread.sleep(3000L);
                    System.out.println("当前线程:" + Thread.currentThread().getName());
                    return "hello world";
                }
            });
            System.out.println("开始时间戳为:" + System.currentTimeMillis());
            futureTask.run();
            String result = futureTask.get();
            System.out.println("结束时间戳为:" + System.currentTimeMillis() + ",result = " + result);
        }
    结果打印:
    开始时间戳为:...59794
    当前线程:main
    结束时间戳为:...62796,result = hello world
    

通过线程池执行,返回Future对象

该方法与上面的方法中方式1的执行效果一样,就是返回结果是Future,实际上也是FutureTask

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        System.out.println("开始时间戳为:" + System.currentTimeMillis());
        Future<String> future = executorService.submit(new Callable<String>() {
            public String call() throws InterruptedException {
                Thread.sleep(3000L);
                System.out.println("当前线程:" + Thread.currentThread().getName());
                return "hello world";
            }
        });
        String result = future.get();
        System.out.println("结束时间戳为:" + System.currentTimeMillis() + ",result = " + result);
    }
结果打印:
开始时间戳为:...55569
当前线程:pool-1-thread-1
结束时间戳为:...58583,result = hello world

原理分析

线程池的submit方法

    // FutureTask传入方式调用的submit方法
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    // Callable传入方式调用的submit方法
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

future.get方法阻塞等待异步线程执行结果

FutureTask内部维护了任务进行的状态,当异步任务完成时,会将状态码设置为已完成,如果发生异常,会将状态码设置成对应的异常状态码。
线程池的使用(结合Future/Callable使用)_第5张图片

    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            // 如果状态还在进行中,或者刚创建,就阻塞等待
            s = awaitDone(false, 0L);
        // 调用Report,返回结果或者抛出异常
        return report(s);
    }

    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            // 状态正常,返回结果
            return (V)x;
        if (s >= CANCELLED)
            // 状态取消,抛出取消异常
            throw new CancellationException();
        // 抛出程序执行异常
        throw new ExecutionException((Throwable)x);
    }












总结

参考博文:

  1. 博文1
  2. 博文2
  3. 博文3

你可能感兴趣的:(Java基础,#,常用(错/忘)小知识,java,jvm,面试)