多线程/并发编程——国庆期间肝了三天两万字的线程池详解

学习线程池之前,首先我们需要明白一个问题——为什么需要线程池?在Java中使用线程来执行异步任务时,线程的创建和销毁需要一定的开销,如果我们为每一个任务都创建一个新的线程来执行的话,这些线程的创建和销毁都需要消耗大量系统资源,同时将会使服务器处于高负荷状态。所以才会出现了线程池,线程池可以理解为使用了原型模式,我们可以提前在线程池中创建若干条线程,当有任务需要执行时,就从线程池中取出一条线程处理任务,线程执行完任务之后也并不会直接销毁,而是重新放在线程池中,等待处理将来可能达到的任务。

如果同一时刻并发量很大,线程池中所有线程都处于忙碌状态,那么后面的任务就进入一个等待队列等待,知道线程池中的线程处理完手头的任务之后,就可以从等待队列中获取需要执行任务并处理。如此循环往复,就不再需要频繁的进行线程的创建和销毁,缓解我们服务器在高并发场景下的压力

一、Executor

在笔者的多线程—Java内存模型与线程文章中,我们探讨了Java中线程的实现是基于操作系统的原生线程模型来实现,即采用 1:1 的线程模型,操作系统会调度所有的线程并将它们分给可用的 CPU,当Java线程终止时,操作系统线程也会被回收。

使用Executor框架进行任务处理的映射方式是这样实现的:在上层,Java多线程服务器程序把大量用户 client 发送的请求分为若干个任务,然后使用Executor调度器将这些任务映射为固定数量的线程;在底层,操作系统会将这些线程映射到哦硬件处理器上,这种任务调度模型如下图所示:

在这里插入图片描述

从图中我们可以看出,应用程序通过Executor框架控制上层的调度,而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。

Executor 是一个接口,其源码如下:

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

Executor 顾名思义,它就是执行器的意思,可以发现其内部只有一个方法 execute(),其功能就是执行任务。以前我们使用线程处理任务,需要new一个线程、重写run()方法、并调用start()方法执行。现在有了 Executor 之后,任务的定义(实现 Runnable 的任务)和任务的执行就可以分离开来,进行异步处理,提高系统响应能力。

二、ExecutorService

ExecutorService 是从Executor接口继承,它除了实现execute() 方法可以执行一个任务之外呢,它还完善了整个任务执行器的生命周期,相当于它拓展了Executor接口。实际上线程池的实现就是基于 ExecutorService 接口的基础之上实现的

ExecutorService 源码如下:

public interface ExecutorService extends Executor {

    //结束
    void shutdown();

    //马上结束
    List<Runnable> shutdownNow();

    //是否结束
    boolean isShutdown();

    //整体是否都执行完了
    boolean isTerminated();

   	//等待结束,指定时间
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    //submit提交一个实现Callable接口的任务,并且返回封装了异步计算结果的Future。
    <T> Future<T> submit(Callable<T> task);

    //submit提交一个实现Runnable接口的任务,并且指定了在调用Future的get方法时返回的result对象。
    <T> Future<T> submit(Runnable task, T result);

    //submit提交一个实现Runnable接口的任务,并且返回封装了异步计算结果的Future。
    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;
}

观察源码我们可以发现,可以发现ExecutorService 接口继承自Executor接口,它必然有execute()方法,用来执行任务,除此之外,它还定义了如下提交任务的方法:

  • Future submit(Callable task):
  • Future submit(Runnable task, T result):
  • Future<?> submit(Runnable task);

主线程使用线程池处理任务,通过调用submit提交任务之后,主线程就该干嘛就干嘛,不需要阻塞在这里等待任务执行结束和获得结果,所以说submit()方法实现了异步处理任务。

上述方法参数中,Runnable 我们是认识的,可是CallableFuture是个什么东西呢?下面我们就来探讨一下

三、Callable、Future和FutureTask

在笔者的多线程—Java内存模型与线程文章中,我们知道创建线程有两种方式,一种是实现Runnable接口,另一种是实现Thread类。但是这两种方式都有个缺点,那就是在任务执行完成之后无法获取返回结果,如果我们想要获取任务返回值结果,应该怎么办呢?

从Java 1.5 之后开始引入了CallableFuture,通过它们构建的线程,在任务执行完毕之后就可以获取结果,下面我们就来聊聊创建线程的第三种方式,那就是实现Callable接口

1、Callable接口

我们先回顾一下java.lang.Runnable接口,此接口只声明了run()方法,返回值为void,当然就无法获取结果了

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

Callable的接口定义如下

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
      V   call()   throws Exception; 
}

此接口声明了一个名称为call()的方法,这个方法有返回值V,同时此方法也能抛出异常。在上面我们知道无论是Runnable接口的实现类还是Callable接口的实现类,都可以被ExecutorService执行,我们再来回顾一下ExecutorService接口提供的方法:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
  • 第一个方法:submit提交一个实现Callable接口的任务,并且返回封装了异步计算结果的Future。
  • 第二个方法:submit提交一个实现Runnable接口的任务,并且指定了在调用Future的get方法时返回的result对象。
  • 第三个方法:submit提交一个实现Runnable接口的任务,并且返回封装了异步计算结果的Future。

因此我们只要创建好我们的线程对象(实现Callable接口或者Runnable接口),然后通过上面3个方法提交给实现了ExecutorService接口的线程池去执行即可。

2、Future接口

Future接口是用来封装异步计算结果的,说白了就是对具体的Runnable或者Callable对象任务执行的结果进行get()获取、cancel()取消、isDone() 判断是否完成等操作。Future接口的源码也不是很难,下面我们来看一下Future接口的源码:

public interface Future<V> {

    //如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;
    boolean cancel(boolean mayInterruptIfRunning);

    //如果任务完成前被取消,则返回true。
    boolean isCancelled();

    //如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
    boolean isDone();

    //获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
    V get() throws InterruptedException, ExecutionException;

    //获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

通过分析我们可以发现实际上Future提供了三种功能:

1. 能够中断执行中的任务;
2. 能够判断任务是否执行完成;
3. 能够获取任务执行完成后的结果。

但是我们需要明白Future只是一个接口,我们无法直接创建一个对象,因此就需要其实现类FutureTask创建对象

3、FutureTask类

FutureTask的源码比较复杂,我们先来简单看一下FutureTask类的源码实现:

public class FutureTask<V> implements RunnableFuture<V> {
       
    public FutureTask(Callable<V> callable) {
        ...
    }

    public FutureTask(Runnable runnable, V result) {
        ...
    }
	...
    public void run() {
   		...
    }
    ......
}

可以发现FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:

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

可以发现FutureTask的类图关系如下:

在这里插入图片描述

源码分析:FutureTask除了实现了Future接口外还实现了Runnable接口,因此FutureTask也可以直接提交给Executor执行。 当然也可以调用线程直接执行(FutureTask.run())。接下来我们根据FutureTask.run()的执行时机来分析任务所处的3种状态:

  1. 未启动:当创建一个FutureTask,但是FutureTask.run()方法还未被执行前,这个FutureTask处于未启动状态;
  2. 已启动FutureTask.run()被执行的过程中,FutureTask处于已启动状态;
  3. 已完成FutureTask.run()方法执行完正常结束,或者被取消或者抛出异常而结束,FutureTask都处于完成状态。

在这里插入图片描述

由于Callable、Future和FutureTask设计出来都是为了构建线程池的,单独很难使用,也很少单独使用,所以这里就不再写演示程序了

四、Executor框架整体结构

学习完CallableFutureFuture Task之后,我们再来探讨一下Executor框架整体结构

Executor框架的结构主要包括3个部分:

  1. 任务Task:被执行的任务需要实现Runnable接口或Callable接口,才可以被Executor框架当作任务执行
  2. 任务的执行:包括任务执行机制的核心接口Executor,以及继承自ExecutorEexcutorService接口。Executor有两个关键类实现了ExecutorService接口(ThreadPoolExecutorScheduledThreadPoolExecutor)。
  3. 异步计算的结果:包括接口Future和实现Future接口的FutureTask

这些类之间的继承关系如下图:

在这里插入图片描述

类图分析总结:

  • Executor是一个接口,它是Executor框架的基础,它将任务的提交很任务的执行分离开来;
  • ThreadPoolExecutor是线程池的核心实现类,用来提交被执行的任务;
  • ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令;
  • Future接口和实现Future接口的FutureTask类,代表异步执行的结果;
  • Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或者 ScheduledThreadPoolExecutor执行。区别就是Runnable无法返回执行结果,而Callable可以返回执行结果。

下面以一张图来理解Executor框架处理任务的执行关系:

在这里插入图片描述

线程池处理任务,流程分析说明:

  1. 主线程首先创建实现Runnable或者Callable接口的任务对象
  2. 把Runnable对象直接提交给ExecutorService执行,方法为ExecutorService.execute(Runnable command)
  3. 或者也可以把Runnable对象或者Callable对象提交给ExecutorService执行,方法为ExecutorService.submit(Runnable task)或者ExecutorService.submit(Callable<T> task)
  4. 由于FutureTask实现了Runnable接口,我们也可以直接创建FutureTask任务,然后提交给ExecutorService执行

到此为止,Executor框架的主要体系结构我们都学习完了。可以知道任务最终都是提交给ExecutorService执行的,而ExecutorService是一个接口,需要有具体的子类实现来执行任务。

下面我们就来重点探讨一下ExecutorService的三个主要的线程池实现类ThreadPoolExecutorScheduledThreadPoolExecutorForkJoinPool,常用的线程池都是基于这两个实现类构建的。

五、ThreadPoolExecutor

传说中的线程池7个参数

ThreadPoolExecutor是线程池的真正实现,通常使用工厂类Executors来创建,但它的构造方法提供了一系列参数来配置线程池,下面我们就先介绍 ThreadPoolExecutor的构造方法中各个参数的含义:

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

重要)构造函数7个参数解释:

  1. corePoolSize:线程池的核心线程数,默认情况下,核心线程数会一直在线程池中存活,即使它们处理闲置状态;
  2. maximumPoolSize:线程池所能容纳的最大线程数量,当活动线程数到达这个数值后,后续的新任务将会被放入阻塞队列;
  3. keepAliveTime:非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收;
  4. TimeUnit:用于指定keepAliveTime参数的时间单位,可以指定为毫秒、秒以及分钟等;
  5. workQueue:线程池中的任务队列,通过线程池的execute方法提交Runnable对象会存储在这个队列中。有关线程池种使用到的阻塞队列请参考:多线程/并发编程——阻塞队列(简要),不同的线程池使用不同的阻塞队列,有各自的应用场景;
  6. ThreadFactory:线程工厂,为线程池提供创建新线程的功能。ThreadFactory是一个接口,它只有一个方法:Thread newThread(Runnable r),产生线程的方式可以通过自己定义一个ThreadFactory,来产生你自己特定的线程。默认提供的是defaultFactory,它创建线程的时候要求指定groupname,这样当出现问题的时候,可以根据日志很快的定位到问题所在;
  7. RejectedExecutionHandler:拒绝策略。当ThreadPoolExecutor已经饱和时(达到了最大线程池大小而且工作队列已经满),execute方法将会调用HandlerrejectExecution方法来通知调用者,我们可以自定义拒绝策略,而默认情况下是抛出一个RejectExecutionException异常;

(重要)了解完相关构造函数的参数,我们再来看看 ThreadPoolExecutor执行任务时的大致处理流程:

  1. 如果线程池的数量还未达到核心线程的数量,那么会直接启动一个核心线程来执行任务;
  2. 如果线程池中的线程数量已经达到或者超出核心线程的数量,那么任务会被插入到任务队列中排队等待执行;
  3. 如果在步骤2中无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务;
  4. 如果在步骤3中线程数量已经达到线程池规定的最大值,那么就会拒绝执行此任务,ThreadPoolExecutor会调用 RejectExecutionHandlerrejectExecution方法来通知调用者。

阿里开发手册要求禁止使用Java提供的线程池,线程池是需要自定义的,所以面试经常会有如下问题:

1、为什么要自定义线程池?

答:创建线程或者线程池时,请指定有意义的线程名称,方便出错时进行回溯。比如你的服务器中有成千上万的线程,有一个线程出错了,在日志中查看出错线程名称是 group1.thread1,根本无法定位到问题之所在,你说崩溃不崩溃。

2、线程池如何自定义创建?

答:阿里开发手册:线程池不允许通过Executors(即Java默认提供的)去创建,而是通过ThreadPoolExecutor(使用上述7个参数)的方式,这样的处理方式让别的成员更加明确线程池的运行规则,规避资源耗尽的问题

说明:Executors返回的线程池对象的弊端:

​ 1):FixedThreadPoolSingleThreadPool

​ 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

​ 2):CachedThreadPool

​ 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

小公司使用线程池可能不会出现上述问题,因为没有那么大的用户群体,但是对于体量比较大的公司,比如阿里、百度、京东等,如果线程池使用不当,完全有可能出现上述问题


ThreadPoolExecutor的使用示例:

public class TestThreadPoolExecutor {

    //定义一个任务 task
    static class Task implements Runnable {
        private int i;
        public Task(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            //打印处理任务的线程名称及任务信息
            System.out.println(Thread.currentThread().getName() + " Task " + i);
            try {
                //阻塞模仿处理任务需要时间,使能正确打印阻塞队列中的任务信息
                System.in.read();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public String toString() {
            return "Task{" +
                    "i=" + i +
                    '}';
        }
    }

    public static void main(String[] args) {
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4,
                60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 8; i++) {
            //线程池处理任务
            tpe.execute(new Task(i));
        }

        //打印队列中的任务
        System.out.println(tpe.getQueue());
        tpe.shutdown();
    }
}

执行结果:

//步骤2:任务添加至阻塞队列
[Task{i=2}, Task{i=3}, Task{i=4}, Task{i=5}]
//步骤1:核心线程处理任务
pool-1-thread-1 Task 0
pool-1-thread-2 Task 1
//步骤3:非核心线程处理任务
pool-1-thread-3 Task 6
pool-1-thread-4 Task 7

到此为止 ThreadPoolExecutor的构造器细节探讨完了, ThreadPoolExecutor的执行规则也探讨完了,也简单模拟了如何使用ThreadPoolExecutor来处理任务。

那么接下来我们就来学习3种常见的线程池,它们都直接或者间接地通过配置 ThreadPoolExecutor来实现自己独特的功能特性,各有各的使用场景,这个3种线程池分别是:

  • FixedThreadPool
  • CachedThreadPool
  • SingleThreadExecutor

1、FixedThreadPool

FixedThreadPool用于创建固定线程数量的线程池

创建 FixedThreadPool对象代码如下:

//通过Executors创建FixedThreadPool线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);

我们来看看 FixedThreadPool创建方法源码:

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

源码分析:可以发现构建此线程池需要传递一个int类型参数,此参数既代表核心线程数有代表最大线程数,所以FixedThreadPool是用来创建一个固定线程数量的线程池

特点:线程数固定,并不会无限制的消耗系统资源,但是其使用LinkedBlockingQueue无界队列来存放任务,有可能导致任务积累过多。但是可以根据业务设置合适数量的线程数,性能比较均衡,所以如果要是使用Java提供的线程池的话,FixedThreadPool是比较普遍使用的线程池

有关线程池中线程数量的设置,有如下建议:
在这里插入图片描述

下面我们来看看FixedThreadPoolexecute()方法的运行流程:

在这里插入图片描述

流程分析:

  1. 如果当前运行线程数少corePoolSize,则创建一个新的线程来执行任务;
  2. 如果当前线程池的运行线程数等于corePoolSize,那么后面提交的任务将加入LinkedBlockingQueue
  3. 线程在执行完图中的1后,会在循环中反复从 LinkedBlockingQueue获取任务来执行。

2、CachedThreadPool

CachedThreadPool 会不断的创建足够多的线程处理任务(Task)

创建CachedThreadPool代码如下:

//通过Executors创建CachedThreadPool线程池
ExecutorService service = Executors.newCachedThreadPool();

CachedThreadPool创建方法代码:

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

源码分析:可以发现其corePoolSize参数指定为0、maximumPoolSize参数指定为Integer.MAX_VALUE,而任务队列使用SynchronousQueue,其容量为0。所以只要有任务到达,CachedThreadPool就会不断地创建新的线程处理任务,直到线程数达到Integer.MAX_VALUE

特点:无核心线程,任务不会阻塞在同步队列,任何任务都会触发新的线程处理

使用场景:要求不会出现卡顿、响应及时的系统中

弊端:如果高并发场景下,服务器服务主线程提交任务速度高于线程池处理任务的速度,CachedThreadPool将会不断的创建新的线程,在极端情况下, CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源

CachedThreadPool 的execute()方法的运行流程:

在这里插入图片描述

流程分析:

  1. 首先执行 SynchronousQueue.offer(Runnable task),添加一个任务。如果当前 CachedThreadPool中有空闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS), 其中NANOSECONDS是毫微秒即十亿分之一秒(就是微秒/1000),那么主线程执行offer操作与空闲线程执行poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则进入第2步;
  2. 当 CachedThreadPool初始线程数为空时,或者当前没有空闲线程,步骤1会失败,此时 CachedThreadPool会创建一个新的线程来执行任务, execute()方法执行完成;
  3. 根据构造参数我们知道指定空闲线程存活时间为60秒,如果步骤2创建的线程需要执行的任务完成后,会调用 SynchronousQueue.poll(),会让空闲线程在线程池中存活60秒,如果在此期间主线程提交了任务,则空闲线程会处理任务;否则,这个空闲线程会将会被终止。由于空闲60秒的线程会被终止,因此哪怕长时间没有任务处理的CachedThreadPool也不会占用系统资源。

总结:由于可能会无限创建新线程,所以实际开发中几乎不会使用这个线程池。

3、SingleThreadPool

SingleThreadPool 见名知意是创建一个线程数为1的线程池

创建SingleThreadPool代码如下:

//通过Executors创建SingleThreadPool线程池
ExecutorService service = Executors.newSingleThreadExecutor();

SingleThreadPool创建方法源码:

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

源码分析:从静态方法可以看出参数corePoolSize和maximumPoolSize被设置为1,其余正常。

特点:线程池中指定只会创建1个线程,LinkedBlockingQueue能够接收的最大任务数为Integer.MAX_VALUE

应用场景:由于只有一个线程,可以保证所有的任务按照先来后到顺序处理,可以用于处理需要保证顺序的任务。而且由于保证只有一个任务会被执行,还可以用来处理存在共享资源而不需要额外考虑加锁同步。

弊端:由于只有一个线程处理任务,如果有任务处理很慢,就会导致任务不断累积到LinkedBlockingQueue无界队列中,最多可累积到Integer.MAX_VALUE数量的任务,导致服务响应很慢,客户端严重卡顿。

在这里插入图片描述

面试问题:既然SingleThreadPool是单线程的线程池,为什么不直接new一个线程处理呢?

答:1、线程池是有任务队列的,如果new一个线程去处理,需要手动维护一个任务队列;

​ 2、线程池是有一个完整的生命周期的,如果new一个线程去处理,需要自己维护线程的创建和销毁。

总结:各自的适用场景

  • FixedThreadPool:适用于为了满足资源管理需求,而需要限制当前线程的数量的应用场景,它适用于负载比较重的服务器;
  • CachedThreadPool:大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载较轻的服务器;
  • SingleThreadExecutor:适用于需要保证执行顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的场景

六、ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,并实现ScheduledExecutorService接口。它主要用来完成在给定的延迟之后执行任务,或者专门用来执行定期任务

1、ScheduledThreadPool

创建ScheduledThreadPool代码如下:

//通过Executors创建ScheduledThreadPool线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);

再来看一下ScheduledThreadPool创建方法源码:

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

可以发现此方法内部return一个ScheduledThreadPoolExecutor,再来看一下其创建方法源码:

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

源码分析:可以发现其核心线程数由传入参数决定,最大线程数为Integer.MAX_VALUE,重点关注其内部存放任务的阻塞队列是DelayedWorkQueue

特点:核心线程数自己决定,但是最大线程数为Integer.MAX_VALUE,内部存放任务的队列是延时队列DelayedWorkQueue

弊端:最大线程数为Integer.MAX_VALUE,如果使用不当,有可能导致无限创建服务线程,导致耗尽CPU系统资源。

应用场景:内部存放任务的阻塞队列为DelayedWorkQueue,用于处理定时任务。

下面来看一下ScheduledThreadPool的执行流程:

在这里插入图片描述

流程分析:

  1. 当调用 ScheduledThreadPoolExecutorscheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向 ScheduledThreadPoolExecutorDelayQueue添加一个实现了RunnableScheduledFuture接口的ScheduleFutureTask
  2. 线程池中的线程从DelayQueue中获取 ScheduleFutureTask,然后执行任务。

使用示例:定时执行任务

public class T10_ScheduledPool {
	public static void main(String[] args) {
		ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
		//此方法功能:实现隔一定时间、以一定频率运行的任务
		service.scheduleAtFixedRate(()->{
			try {
				TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName());
		}, 0, 500, TimeUnit.MILLISECONDS);//以500毫秒运行一次任务
		
	}
}

关于处理定时任务,我们一般也很少使用ScheduledThreadPool线程池,因为如果是简单的定时任务,直接使用Java自带的Timer;如果是复杂的定时任务,应该使用更加专业的定时框架如quartz。所以对于ScheduledThreadPool做到了解即可。

七、ForkJoinPool

使用场景:

  • 分解、汇总的任务
  • 用很少的线程可以执行很多的任务
  • CPU密集型操作

1、ForkJoinPool

ForkJoinPool的核心思想就是分治思想。

把大任务不断的切分(fork)成一定规模的小任务,小任务执行完的结果不断汇总(join),得到我们最终所求的结果。

在这里插入图片描述

创建ForkJoinPool线程池对象:

ForkJoinPool fjp = new ForkJoinPool();

创建ForkJoinPool线程池方法源码:

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

源码分析:创建ForkJoinPool的时候,要求必须定义为指定的类型。原来的任务要求是Runnable或者Callable类型,ForkJoinPool要求任务必须是能进行分治的任务,这个任务类型是ForkJoinTask,不过由于ForkJoinTask比较原始,现在一般使用其子类RecursiveAction,观其名字我们就能知道这是一个支持递归过程的任务。

使用ForkJoinPool讲任务分治的示例:

public class TestForkJoinPool {
	//长度为一百万的数组,里面数值都是通过Random随机产生的
	//问题:要求对这个数组中所有的数进行数值的总和计算
	static int[] nums = new int[1000000];
	static Random r = new Random();

	//使用ForkJoinPool将大任务拆分成的baseCase为五万长度
	static final int MAX_NUM = 50000;

	//单线程计算:通过stream流完成单线程的累加过程,耗时很长
	static {
		for(int i=0; i<nums.length; i++) {
			nums[i] = r.nextInt(100);
		}
		System.out.println("单线程计算结果:" + Arrays.stream(nums).sum()); //stream api
	}

	//使用ForkJoinPool多线程处理任务
	static class AddTaskRet extends RecursiveTask<Long> {
		private static final long serialVersionUID = 1L;
		int start, end;
		
		AddTaskRet(int s, int e) {
			start = s;
			end = e;
		}

		@Override
		protected Long compute() {
			//如果数组长度低于50000,就进行数组元素累加求和
			if(end-start <= MAX_NUM) {
				long sum = 0L;
				for(int i=start; i<end; i++) sum += nums[i];
				return sum;
			}
			//如果数组长度唱过50000,就不断进行二分
			int middle = start + (end-start)/2;

			AddTaskRet subTask1 = new AddTaskRet(start, middle);
			AddTaskRet subTask2 = new AddTaskRet(middle, end);
			subTask1.fork();
			subTask2.fork();
			
			return subTask1.join() + subTask2.join();
		}
		
	}
	
	public static void main(String[] args) throws IOException {
		//创建ForkJoinPool
		ForkJoinPool fjp = new ForkJoinPool();
		//定义任务
		AddTaskRet task = new AddTaskRet(0, nums.length);
		//执行任务
		fjp.execute(task);
		//得到并输出结果,随机产生数值,故每次运行结果都不相同
		//但是单线程和多线程获得的结果一定是相同的
		long result = task.join();
		System.out.println("多线程计算结果:" + result);
	}
}

输出结果:

单线程计算结果:49517577
多线程计算结果:49517577

有关ForkJoinPool内部实现比较复杂,这里目前先略过。

2、WorkStealingPool

前面讲的基于ThreadPoolExecutor创建的线程池都是有线程队列和任务队列,线程队列不断从任务队列里面获取任务并处理,而WorkStealingPool线程池中的每一个线程都有各自的一个任务队列,当其中一个线程中的所有任务都处理完之后,它可以从别的线程的任务队列中获取任务并处理,可以帮助其他线程分担压力。

常规基于ThreadPoolExecutorWorkStealingPool的对比:

在这里插入图片描述

WorkStealingPool中的线程从别的线程中分担任务流程:

在这里插入图片描述

创建WorkStealingPool线程池代码如下:

//通过Executors创建WorkStealingPool线程池
ExecutorService service = Executors.newWorkStealingPool();

创建过程:

public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

源码分析:可以发现创建WorkStealingPool其实就是创建一个ForkJoinPool并返回,所以只要理解了ForkJoinPool的创建过程,其实也就理解了WorkStealingPool的创建过程。


补充:

1、线程池分类

在这里插入图片描述

2、阻塞队列

在这里插入图片描述


关于线程池的学习到此结束,由于笔者能力有限,目前大致只能做出以上总结,但是相信如果能理解以上总结的知识,应对常规的面试应该绰绰有余了。

有关多线程、并发编程的所有知识点目前都总结完了,短期之内应该不会再有更新了,相关系列文章在下面都有链接。如果对你有帮助,欢迎点赞、评论以及转发呦,转载只要标明出处即可。


关联文章:

多线程—Java内存模型与线程

多线程——Volatile 关键字详解

多线程——线程安全及实现机制

多线程——深入剖析 Synchronized

多线程\并发编程——ReentrantLock 详解

多线程/并发编程——CAS、Unsafe及Atomic

多线程/并发编程——两万字详解AQS

多线程/并发编程——同步工具类(CountDownLatch、Semaphore、ReadWriteLock、CyclicBarrier )

多线程/并发编程——面试再也不怕 ThreadLocal 了

多线程/并发编程——阻塞队列(简要)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值