线程池和 Executor框架

目录

一 使用线程池的好处

二 Executor 框架

2.1 简介

2.2 Executor 框架结构(主要由三大部分组成)

2.3 Executor 框架的使用示意图

三 ThreadPoolExecutor详解

3.1 ThreadPoolExecutor类的四个比较重要的属性

3.2 ThreadPoolExecutor类中提供的四个构造方法

 

3.3 如何创建ThreadPoolExecutor

3.4 FixedThreadPool详解

3.5 SingleThreadExecutor详解

3.6 CachedThreadPool详解

 

五 各种线程池的适用场景介绍

六 总结


一 使用线程池的好处

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。 这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

二 Executor 框架

2.1 简介

Executor 框架是Java5之后引进的,在Java 5之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。

补充:this逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。

2.2 Executor 框架结构(主要由三大部分组成)

1 任务。

执行任务需要实现的Runnable接口Callable接口。 Runnable接口Callable接口实现类都可以被ThreadPoolExecutorScheduledThreadPoolExecutor执行。

两者的区别:

Runnable接口不会返回结果但是Callable接口可以返回结果。后面介绍Executors类的一些方法的时候会介绍到两者的相互转换。

2 .任务的执行

如下图所示,包括任务执行机制的核心接口Executor ,以及继承自Executor 接口的ExecutorService接口ScheduledThreadPoolExecutorThreadPoolExecutor这两个关键类实现了ExecutorService接口

注意: 通过查看ScheduledThreadPoolExecutor源代码我们发现ScheduledThreadPoolExecutor实际上是继承了ThreadPoolExecutor并实现了ScheduledExecutorService ,而ScheduledExecutorService又实现了ExecutorService,正如我们下面给出的类关系图显示的一样。

ThreadPoolExecutor类描述:

//AbstractExecutorService实现了ExecutorService接口

public class ThreadPoolExecutor extends AbstractExecutorService

ScheduledThreadPoolExecutor类描述:

//ScheduledExecutorService实现了ExecutorService接口

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor
 implements  ScheduledExecutorService
 

 

3 异步计算的结果

Future接口以及Future接口的实现类FutureTask类。 当我们把Runnable接口Callable接口的实现类提交(调用submit方法)给ThreadPoolExecutorScheduledThreadPoolExecutor时,会返回一个FutureTask对象

我们以AbstractExecutorService接口中的一个submit方法为例子来看看源代码:

 /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

上面的例子通过 newTaskFor 返回一个RunnableFuture对象

/**
     * Returns a {@code RunnableFuture} for the given runnable and default
     * value.
     *
     * @param runnable the runnable task being wrapped
     * @param value the default value for the returned future
     * @param <T> the type of the given value
     * @return a {@code RunnableFuture} which, when run, will run the
     * underlying runnable and which, as a {@code Future}, will yield
     * the given value as its result and provide for cancellation of
     * the underlying task
     * @since 1.6
     */
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

经过包装后, 执行execute

2.3 Executor 框架的使用示意图

 1. 主线程首先要创建实现Runnable或者Callable接口的任务对象。 备注: 工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)或Executors.callable(Runnable task,T result))。

 2. 然后可以把创建完成的Runnable对象直接交给ExecutorService

执行execute()方法和submit()方法的区别是什么呢? 1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否; 2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

  3. 如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象(我们刚刚也提到过了执行execute()方法和submit()方法的区别,到目前为止的JDK中,返回的是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行。

 4. 最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

ice执行(ExecutorService.execute(Runnable command));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(ExecutorService.submit(Runnable task)或ExecutorService.submit(Callable task))。

具体代码实例可以参考我的其他文章,传送门

三 ThreadPoolExecutor详解

线程池实现类ThreadPoolExecutor是Executor 框架最核心的类,先来看一下这个类中比较重要的四个属性

3.1 ThreadPoolExecutor类的四个比较重要的属性

3.2 ThreadPoolExecutor类中提供的四个构造方法

我们看最长的那个,其余三个都是在这个构造方法的基础上产生(给定某些默认参数的构造方法)

    
    /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     * @param keepAliveTime 当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,
     *核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
     * @param unit  keepAliveTime参数的时间单位
     * @param workQueue 等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一个Worker对象放入等待队列;
     * @param threadFactory 执行者创建新线程时使用的工厂
     * @param handler RejectedExecutionHandler类型的变量,表示线程池的饱和策略。
     * 如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。
     * 线程池提供了4种策略:
        1.AbortPolicy:直接抛出异常,这是默认策略;
        2.CallerRunsPolicy:用调用者所在的线程来执行任务;
        3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
        4.DiscardPolicy:直接丢弃任务;
     */
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;
    }

 

3.3 如何创建ThreadPoolExecutor

方式一:通过构造方法实现(官方API文档并不推荐,所以建议使用第二种方式) 方式二:通过Executor 框架的工具类Executors来实现 我们可以创建三种类型的ThreadPoolExecutor:

  • FixedThreadPool

  • SingleThreadExecutor

  • CachedThreadPool

对应Executors工具类中的方法如图所示: 

3.4 FixedThreadPool详解

FixedThreadPool被称为可重用固定线程数的线程池。通过Executors类中的相关源代码来看一下相关实现:


    /**
     * 创建一个可重用固定数量线程的线程池
     *在任何时候至多有n个线程处于活动状态
     *如果在所有线程处于活动状态时提交其他任务,则它们将在队列中等待,
     *直到线程可用。 如果任何线程在关闭之前的执行期间由于失败而终止,
     *如果需要执行后续任务,则一个新的线程将取代它。池中的线程将一直存在

     *知道调用shutdown方法
     * @param nThreads 线程池中的线程数
     * @param threadFactory 创建新线程时使用的factory
     * @return 新创建的线程池
     * @throws NullPointerException 如果threadFactory为null
     * @throws IllegalArgumentException if {@code nThreads <= 0}
     */
 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
 }

另外还有一个FixedThreadPool的实现方法,和上面的类似,所以这里不多做阐述:

从上面源代码可以看出新创建的FixedThreadPool的corePoolSize和maximumPoolSize都被设置为nThreads。 FixedThreadPool的execute()方法运行示意图(该图片来源:《Java并发编程的艺术》):

上图说明:

  1. 如果当前运行的线程数小于corePoolSize,则创建新的线程来执行任务;

  2. 当前运行的线程数等于corePoolSize后,将任务加入LinkedBlockingQueue;

  3. 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行;

FixedThreadPool使用无界队列 LinkedBlockingQueue(队列的容量为Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响:

  1. 当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize;

  2. 由于1,使用无界队列时maximumPoolSize将是一个无效参数;

  3. 由于1和2,使用无界队列时keepAliveTime将是一个无效参数;

  4. 运行中的FixedThreadPool(未执行shutdown()或shutdownNow()方法)不会拒绝任务

3.5 SingleThreadExecutor详解

SingleThreadExecutor是使用单个worker线程的Executor。下面看看SingleThreadExecutor的实现:

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

从上面源代码可以看出新创建的SingleThreadExecutor的corePoolSize和maximumPoolSize都被设置为1.其他参数和FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Intger.MAX_VALUE)。SingleThreadExecutor使用无界队列作为线程池的工作队列会对线程池带来的影响与FixedThreadPool相同。

SingleThreadExecutor的运行示意图(该图片来源:《Java并发编程的艺术》): 

上图说明;

  1. 如果当前运行的线程数少于corePoolSize,则创建一个新的线程执行任务;

  2. 当前线程池中有一个运行的线程后,将任务加入LinkedBlockingQueue

  3. 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue中获取任务来执行;

3.6 CachedThreadPool详解

CachedThreadPool是一个会根据需要创建新线程的线程池。下面通过源码来看看 CachedThreadPool的实现:

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

CachedThreadPool的corePoolSize被设置为空(0),maximumPoolSize被设置为Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新的线程。极端情况下,这样会导致耗尽cpu和内存资源。

CachedThreadPool的execute()方法的执行示意图(该图片来源:《Java并发编程的艺术》): 

上图说明:

  1. 首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤2;

  2. 当初始maximumPool为空,或者maximumPool中没有空闲线程时,将没有线程执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1将失败,此时CachedThreadPool会创建新线程执行任务,execute方法执行完成;

由于在阿里java代码开发规范中规定, 不能使用executors创建线程池,所以我这边用一个标准的例子写了一个demo

package com.zz.amqp1.multithread;

import com.google.common.collect.Lists;
import org.junit.Test;

import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Description:性能较好的线程池
 * User: zhouzhou
 * Date: 2018-12-04
 * Time: 5:14 PM
 */
public class ThreadPoolTest {

    private static final int count = 14;

    @Test
    public void tsetSimpleThreadPool() throws Exception {
        Long start = System.currentTimeMillis();

        // 5个核心线程, 队列为10个长度, 超过10个回归主线程走
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5, 10, 60, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(10), new ThreadPoolExecutor.CallerRunsPolicy());

        List<Future> list = Lists.newArrayList();
        // 小哥哥来五发操作
        for (int i = 0; i < count; i++) {
            list.add(doExecute(threadPoolExecutor));
        }

        // 遍历抽奖结果
        for (Future future : list) {
            System.out.println(future.get());
        }

        System.out.println(String.format("操作时间共计为:{%s}毫秒", System.currentTimeMillis() - start));

    }

    // 模拟2秒抽奖
    private class TestTask implements Callable<String> {

        @Override
        public String call() throws Exception {
            // 休息2秒
            TimeUnit.SECONDS.sleep(2);
            return String.format("随机号码为: {%s}", new Random().nextInt(100));
        }
    }

    // 线程池执行任务
    private Future<String> doExecute(ThreadPoolExecutor threadPoolExecutor) {
        return threadPoolExecutor.submit(new TestTask());
    }
}

 

五 各种线程池的适用场景介绍

FixedThreadPool: 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较重的服务器;

SingleThreadExecutor: 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的应用场景。

CachedThreadPool: 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器;

ScheduledThreadPoolExecutor: 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限制后台线程的数量的应用场景,

SingleThreadScheduledExecutor: 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务的应用场景。

六 总结

本节只是简单的介绍了一下使用线程池的好处,然后花了大量篇幅介绍Executor 框架。详细介绍了Executor 框架中ThreadPoolExecutor和ScheduledThreadPoolExecutor,并且通过实例详细讲解了ScheduledThreadPoolExecutor的使用。对于FutureTask 只是粗略带过,因为篇幅问题,并没有深究它的原理,后面的文章会进行补充。这一篇文章只是大概带大家过一下线程池的基本概览,深入讲解的地方不是很多,后续会通过源码深入研究其中比较重要的一些知识点。

最后,就是这两周要考试了,会抽点时间出来简单应付一下学校考试了。然后,就是写这篇多线程的文章废了好多好多时间。一直不知从何写起。

猜你喜欢

转载自blog.csdn.net/weixin_38399962/article/details/85599196