线程池原理整合

一、基本架构


在我们使用线程就去创建一个线程,如果并发的线程过多,频繁的创建线程就会大大降低系统的效率(创建线程需要时间)

有了线程池,每有一个任务,从线程池调度一个空闲的线程来执行任务,就避免了每次都去创建线程带来的开销

一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

 1、Executor

一个运行新任务的简单接口

public interface Executor {
//在将来的某个时间执行给定的命令。 该命令可以在一个新线程,一个合并的线程中或在调用线程中执行,由Executor实现。 
    void execute(Runnable command);
}

2、ExecutorService

扩展了Executor接口。添加了一些用来管理执行器生命周期和任务生命周期的方法

3. AbstractExecutorService

该类实现了ExecutorService接口,为线程池提供了默认实现,是线程池的基础骨架。

4. ThreadPoolExecutor

该类就是JAVA的线程池,继承了AbstractExecutorService

5. ScheduledExecutorService

该接口提供了延时和周期性的任务执行功能

6. ScheduledThreadPoolExecutor

带延时和周期性任务执行的线程池

7. Executors

线程池的静态工厂,通过该静态工厂返回常用的线程池

二、ThreadPoolExecutor


ThreadPoolExecutor就是我们经常说的大名鼎鼎的线程池,Executors工厂创建的线程池都是该类的实例,通过调节参数的大小创建适用于各个场景的线程池。ThreadPoolExecutor继承了AbstractExecutorService,该抽象类为线程池提供了默认实现

其实java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。


构造函数

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;
}

这个构造函数有很多参数,我们分别来解释每一个参数的意义

  • corePoolSize:核心线程数,当提交一个新的任务到线程池,如果当前线程池运行的线程数(包括闲置的线程)小于核心线程数,则会创建一个新的线程作为核心线程来执行该任务。
  • maximumPoolSize:线程池允许最大的线程数,当提交一个新的任务到线程池,如果当前线程池运行的线程数(包括闲置的线程)大于corePoolSize,小于maximumPoolSize,并且等待队列满的时候,会创建一个新的线程来处理该任务。
  • keepAliveTime:当线程池中线程数量大于corePoolSize时,闲置线程最长可以存活的时间
  • unit:时间单位。
  • workQueue:保存任务的队列,当池中线程数大于corePoolSize时,新来的任务保存到该队列。
  • threadFactory:线程工厂,线程池中的线程都是通过这个工厂创建的。
  • handler:任务拒绝执行策略,当线程池无法处理新来任务时的处理策略。
  • handler的拒绝策略:

    有四种:第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满

                 第二种DisCardPolicy:不执行新任务,也不抛出异常

                 第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行

                 第四种CallerRunsPolicy:直接调用execute来执行当前任务

讲到这,有必要讲下ThreadPoolExecutor的设计思路,了解了设计思路对后面源码的分析会有更好的效果。

通过上图我们介绍下线程池的设计思路:

  1. 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务。
  2. 如果当前池中线程数大于等于coolPoolSize,则将该任务加入到等待队列。
  3. 如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。
  4. 如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理,后面还会详细讲解具体的拒绝执行策略。

以上4步是线程池处理到达任务的主要流程。当池中线程数大于coolPoolSize,超过keepAlive时间的闲置线程会被回收掉。注意,回收的是非核心线程,核心线程一般是不会回收的。如果设置allowCoreThreadTimeOut(true),则核心线程在闲置keepAlive时间后也会被回收。

任务队列是一个阻塞队列,线程执行完任务后会去队列取任务来执行,如果队列为空,线程就会阻塞,直到取到任务。

接下来继续学习源码:

submit方法

通常情况下我们通过submit方法向线程池提交并执行任务,线程池的submit方法都是在子类AbstractExecutorService实现的,并且有多个重载的方法,看下这些方法的实现:

    //提交Runnable的任务,通过newTaskFor方法包装成RunnableFuture
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
       //指定返回值为null
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }


    //提交Runnable的任务,指定返回值为result
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

  //提交Callable的任务
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

这几个重载方法首先通过newTaskFor方法将任务包装成一个RunnableFuture,然后调用execute方法执行任务。

先看下newTaskFor方法:

   protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

这两个重载方法很简单,创建一个FutureTask对象返回,之后我们就可以通过这个对象的get方法获取任务执行结果了。

AbstractExecutorService这几个重载的submit方法最终都调用了execute方法,通过该方法真正执行任务。

execute方法

execute是真正执行任务的方法,分析execute源码之前先来看下ThreadPoolExecutor的状态字段定义:

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //COUNT_BITS计算后等于29,活动线程数占用的位数
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //活动线程最大数量
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;


    // runState is stored in the high-order bits
    //线程池5种运行状态,保存在ctl高3位
    //11111111 11111111 11111111 11111111左移29位后只保留高位3个1即:
    //11100000 00000000 00000000 00000000
    private static final int RUNNING    = -1 << COUNT_BITS;
    //0左移29位后
    //00000000 00000000 00000000 00000000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    //1左移29位后
    //00100000 00000000 00000000 00000000
    private static final int STOP       =  1 << COUNT_BITS;
    //2左移29位后
    //01000000 00000000 00000000 00000000
    private static final int TIDYING    =  2 << COUNT_BITS;
    //3左移29位后
    //01100000 00000000 00000000 00000000
    private static final int TERMINATED =  3 << COUNT_BITS;

线程池维护了一个int原子变量ctl,表示线程池当前状态。通过这一个字段表示线程池当前活动线程数和线程池的运行状态。其中低29位用来表示活动线程数高3位用来表示线程池的运行状态

线程池的运行状态有RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。状态所对应的数字其实没有什么意义,重点需要了解状态代表的含义。

  •     RUNNING:该状态下的线程池可以接受新任务,并且可以处理等待队列中的任务
  •     SHUTDOWN:该状态下的线程池不再接受新任务,但是可以处理等待队列中的任务
  •     STOP:该状态下的线程池不再接受新任务,不再处理等待队列中的任务,会中断正在执行的任务
  •     TIDYING:所有的任务都已经中止,活动线程数为0,此状态下的线程池即将转移到TERMINATED状态。
  •     TERMINATED:terminated()执行完后到达此状态。

线程池的状态转移包括如下几个:

  •     RUNNING -> SHUTDOWN,在执行shutdown()方法时,线程池经历了这种状态转移过程。
  •     RUNNING -> STOP或者SHUTDOWN -> STOP,在执行shutdownNow()方法时,线程池经历了这种状态转移过程。
  •     SHUTDOWN -> TIDYING,当等待队列和池中的任务都为空时,经历了这种状态转移过程。
  •     STOP -> TIDYING,池中任务为空时,经历这种状态转移过程。
  •     TIDYING -> TERMINATED,执行terminated()方法时经历这个状态转移过程。
     

接下来看execute的源码: 

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // clt记录着runState和workerCount
    int c = ctl.get();
    //workerCountOf方法取出低29位的值,表示当前活动的线程数
    //然后拿线程数和 核心线程数做比较
    if (workerCountOf(c) < corePoolSize) {
        // 如果活动线程数<核心线程数
        // 添加到
        //addWorker中的第二个参数表示限制添加线程的数量是根据corePoolSize来判断还是maximumPoolSize来判断
        if (addWorker(command, true))
            // 如果成功则返回
            return;
        // 如果失败则重新获取 runState和 workerCount
        c = ctl.get();
    }
    // 如果当前线程池是运行状态并且任务添加到队列成功
    if (isRunning(c) && workQueue.offer(command)) {
        // 重新获取 runState和 workerCount
        int recheck = ctl.get();
         //重新判断线程池是否处于RUNNING状态,若不处于RUNNING状态,删除等待队列中该任务并拒绝任务
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            //第一个参数为null,表示在线程池中创建一个线程,但不去启动
            // 第二个参数为false,将线程池的有限线程数量的上限设置为maximumPoolSize
            addWorker(null, false);
    }
    //任务添加到等待队列失败,尝试创建一个非核心线程执行该任务,创建失败则拒绝执行任务
    //但第二个参数传入为false,将线程池的有限线程数量的上限设置为maximumPoolSize
    else if (!addWorker(command, false))
        //如果失败则拒绝该任务
        reject(command);
}

//当前活动线程数量 
private static int workerCountOf(int c) {
    //c & 00011111 11111111 11111111 11111111 
    //"与"运算取低29位的值 
    return c & CAPACITY; 
}

execute的执行逻辑其实前面已经提到了,这里根据代码再分析下:

  1.     对于空的任务,线程池会抛出NPE(空指针)异常
  2.     通过workerCountOf方法获取线程池的线程数,若线程数小于核心线程数,创建一个核心线程并将任务作为该核心线程的第一个任务。若创建线程失败,重新获取线程池状态。
  3.     尝试将任务添加到等待队列,需要注意的是,任务添加到等待队列成功后,需要进一步检查线程池状态,因为这个过程线程池的状态可能已经改变。
  4.     尝试将任务添加到等待队列,添加失败拒绝执行任务。

workCountOf方法很简单,通过”与”运算取ctl的低29位的值。

看下addWorker方法,该方法是线程池的重点:

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {

        int c = ctl.get();
        //  获取运行状态
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        // 如果状态值 >= SHUTDOWN (不接新任务&不处理队列任务)
        // 并且 如果 !(rs为SHUTDOWN 且 firsTask为空 且 阻塞队列不为空)
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            // 返回false
            return false;

        for (;;) {
            //获取线程数wc
            int wc = workerCountOf(c);
            // 如果wc大与容量 || core如果为true表示根据corePoolSize来比较,否则为maximumPoolSize
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 增加workerCount(原子操作)
            if (compareAndIncrementWorkerCount(c))
                // 如果增加成功,则跳出
                break retry;
            // wc增加失败,则再次获取runState
            c = ctl.get();  // Re-read ctl
            // 如果当前的运行状态不等于rs,说明状态已被改变,返回重新执行
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // 根据firstTask来创建Worker对象
        w = new Worker(firstTask);
        // 根据worker创建一个线程
        final Thread t = w.thread;
        if (t != null) {
            // new一个锁
            final ReentrantLock mainLock = this.mainLock;
            // 加锁
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                // 获取runState
                int rs = runStateOf(ctl.get());
                // 如果rs小于SHUTDOWN(处于运行)或者(rs=SHUTDOWN && firstTask == null)
                // firstTask == null证明只新建线程而不执行任务
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    // 如果t活着就抛异常
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // 否则加入worker(HashSet)
                    //workers包含池中的所有工作线程。仅在持有mainLock时访问。
                    workers.add(w);
                    // 获取工作线程数量
                    int s = workers.size();
                    //largestPoolSize记录着线程池中出现过的最大线程数量
                    if (s > largestPoolSize)
                        // 如果 s比它还要大,则将s赋值给它
                        largestPoolSize = s;
                    // worker的添加工作状态改为true    
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // 如果worker的添加工作完成
            if (workerAdded) {
                // 启动线程
                t.start();
                // 修改线程启动状态
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    // 返回线启动状态
    return workerStarted;

注释已经很详细了,这里再说下该方法的思路:

  •     首先试图原子地增加线程数,这个过程需要检查ctl的状态,如果检查发现不能创建新worker,返回false。否则自旋CAS增加线程数,直到设置成功。
  •     线程数增加成功后,真正创建worker并添加到workers工作集合中。创建worker成功后,启动该工作者线程,返回是否启动成功。如果启动worker失败,需要做回滚操作,从workers中移除该worker,并将wc减1。

为什么要用线程池?

简洁的答两点就行。

  1. 降低系统资源消耗。

  2. 提高线程可控性。

如何创建使用线程池?

JDK8提供了五种创建线程池的方法:

1.创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

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

2.(JDK8新增)会根据所需的并发数来动态创建和关闭线程。能够合理的使用CPU进行对任务进行并发操作,所以适合使用在很耗时的任务。

注意返回的是ForkJoinPool对象。

public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

什么是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();
    }

使用一个无限队列来保存需要执行的任务,可以传入线程的数量;不传入,则默认使用当前计算机中可用的cpu数量;使用分治法来解决问题,使用fork()和join()来进行调用。

3.创建一个可缓存的线程池,可灵活回收空闲线程,若无可回收,则新建线程。

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

4.创建一个单线程的线程池。

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

5.创建一个定长线程池,支持定时及周期性任务执行。

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

 高频考点:

  1. 创建线程池的五个方法。

  2. 线程池的五个状态

  3. execute执行过程。

  4. runWorker执行过程

 https://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247487936&idx=1&sn=5c9795ad21e22080cb12cc5c0153208a&chksm=ebd62eecdca1a7fae5dd1f79bbff4a102bb1f9b6058d77e3cfc2be5e19c10edd9a3920d13d01&mpshare=1&scene=23&srcid=#rd

https://blog.csdn.net/luanmousheng/article/details/77688356

猜你喜欢

转载自blog.csdn.net/qq_40949465/article/details/88936419