• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

java8线程池

java 来源:npjtwy 7次浏览

java线程的创建、销毁和线程减切换是一件比较耗费计算机资源的事。如果我们需要用多线程处理任务,并频繁的创建、销毁线程会造成计算机资源的无端浪费,因此出现了线程池技术。在《java并发编程的艺术》一书中,作者总结了三条使用线程池的好处:

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

线程池的使用

线程池的创建

线程池的创建可以通过创建 ThreadPoolExecutor 对象或者调用 Executors 的工厂方法来创建线程池。但是在阿里巴巴的java开发手册中提到:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。

ThreadPoolExecutor

因此先看一下怎么通过创建 ThreadPoolExecutor 对象来创建一个线程池。

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

这是 ThreadPoolExecutor 的构造方法,其中的参数含义如下:

  • corePoolSize:核心线程池大小, 当新的任务到线程池后,线程池会创建新的线程(即使有空闲线程),直到核心线程池已满。
  • maximumPoolSize:最大线程池大小,顾名思义,线程池能创建的线程的最大数目
  • keepAliveTime:程池的工作线程空闲后,保持存活的时间
  • TimeUnit: 时间单位
  • BlockingQueue<Runnable>:用来储存等待执行任务的队列
  • threadFactory:线程工厂
  • RejectedExecutionHandler: 当队列和线程池都满了时拒绝任务的策略

重要参数的说明:

  • corePoolSize 和 maximumPoolSize
    默认情况下线程中的线程初始时为 0, 当有新的任务到来时才会创建新线程,当线程数目到达 corePoolSize 的数量时,新的任务会被缓存到 workQueue 队列中。如果不断有新的任务到来,队列也满了的话,线程池会再新建线程直到总的线程数目达到 maximumPoolSize。如果还有新的任务到来,则要根据 handler 对新的任务进行相应拒绝处理。

  • BlockingQueue<Runnable>
    一个阻塞队列,用来存储等待执行的任务,常用的有如下几种:

    1. ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    2. LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    3. SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    4. PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
  • RejectedExecutionHandler
    当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。有下面四种JDK提供的策略:

    1. AbortPolicy,表示无法处理新任务时抛出异常, 默认策略
    2. CallerRunsPolicy:用调用者所在线程来运行任务。
    3. DiscardOldestPolicy: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
    4. DiscardPolicy:不处理,丢弃掉
      除了这些JDK提供的策略外,还可以自己实现 RejectedExecutionHandler 接口定义策略。

一个创建线程池的小例子:

public class CreateThreadPool { 
    public static void main(String args[]) {
        //不建议的做法
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        //使用 guava 开源框架的 ThreadFactoryBuilder 给线程池的线程设置名字
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-thread-%d").build();

        ExecutorService pool = new ThreadPoolExecutor(4, 10, 0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingDeque<Runnable>(256),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());

        pool.execute(() -> System.out.println(Thread.currentThread().getName()));
        pool.execute(() -> System.out.println(Thread.currentThread().getName()));
        pool.execute(() -> System.out.println(Thread.currentThread().getName()));
        pool.shutdown();
    }
}

//输出:
demo-thread-0
demo-thread-1
demo-thread-2

使用 Executors 的工厂方法创建线程

1. SingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

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

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

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

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小, 极端情况下会因为创建过多线程而耗尽系统资源

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

这里虽然指定 maximumPool 为 Integer.MAX_VALUE,但没什么意义,如果不能满足任务执行需求,CachedThreadPool 还会继续创建新的线程。

4. ScheduledThreadPool

主要用来在给定的延迟之后运行任务,或者定期执行任务。

public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }
5. newWorkStealingPool

newWorkStealingPool 是jdk1.8才有的,会根据所需的并行层次来动态创建和关闭线程,通过使用多个队列减少竞争,底层用的 ForkJoinPool来实现的。ForkJoinPool 的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。

五种线程池的使用场景

  • newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
  • newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
  • newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
  • newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
  • newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。

线程池的关闭

shutdown

shutdown 的原理是只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。

shutdownNow

shutdownNow 的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow 会首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

通常调用 shutdown 来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow

线程池的原理

一个线程池处理的基本流程如下(jdk7之前):

  1. 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
  2. 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
  3. 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

jdk7之后的线程池处理流程更复杂、判断条件也更多:

execute

结合 ThreadPoolExecutor 类的源码和图看一下具体执行流程。

ThreadPoolExecutor 类中,最核心的任务提交方法是 execute() 方法:

public void execute(Runnable command) {
    //判断提交的任务是否为 null, 是则抛出异常
    if (command == null)
        throw new NullPointerException();

    /* * 获取线程池控制状态 * ctl 是一个 AtomicInteger 变量 (骚操作) * jdk 8 中通过一个 int 值的前 28 位表示工作线程数量 workerCount, 剩余高位来表示 线程池状态 * 计算 workerCount 和 runState 时通过掩码计算。 CAPACITY = (1 << 29) - 1 * private static int runStateOf(int c) { return c & ~CAPACITY; } * private static int workerCountOf(int c) { return c & CAPACITY; } * */
    int c = ctl.get();

    //1. 当线程数小于 核心线程池容量时 将添加工作线程去执行任务

    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return; 
        c = ctl.get(); // 不成功则再次获取线程池控制状态
    }

    //2. (worker线程数量大于核心线程池容量时)如果线程池处于 RUNNING 状态,将命令加入 workQueue 队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get(); //再次检查防止状态突变
        if (! isRunning(recheck) && remove(command)) 
            //2.1 如果状态改变,线程池没有 RUNNING 则将命令移出队列,并拒绝执行
            reject(command); 
        else if (workerCountOf(recheck) == 0) 
            //2.2 状态没有改变,线程池 RUNNING,但 worker线程数量为 0, 则添加非core的worker线程
             addWorker(null, false);
    }

    //3. 如果线程池没有 RUNNING 并尝试添加非core的 worker 线程失败,那就拒绝执行
    else if (!addWorker(command, false)) 
        reject(command);
}
  1. 如果运行的线程小于 corePoolSize,则尝试使用用户定义的 Runnalbe 对象创建一个新的线程。调用 addWorker() 函数会原子性的检查runStateworkCount,通过返回 false 来防止在不应该添加线程时添加了线程
  2. 如果一个任务能够成功入队列,在添加一个线程时仍需要进行双重检查(可能因为在前一次检查后该线程死亡了),或者当进入到此方法时,线程池已经shutdown了,所以需要再次检查状态。若线程此时的状态不是 RUNNING,则需要回滚入队列操作;或者当线程池没有工作线程时,需要创建一个新的工作线程。
  3. 如果无法入队列,那么需要增加一个新工作线程,如果此操作失败,那么就意味着线程池已经 SHUTDOWN 或者已经饱和了,所以拒绝任务

上面提到了线程池的状态,那就来看一下:

  • RUNNING = -1 << COUNT_BITS; //运行状态, 也是线程池的初始状态
  • SHUTDOWN = 0 << COUNT_BITS; //不再接收新的任务,但是会处理队列中的任务
  • STOP = 1 << COUNT_BITS; // 不再接收新的任务,也不会处理队列中的任务并且会中断正在执行的任务
  • TIDYING = 2 << COUNT_BITS; // 所有线程已经终止,并且工作线程数 workerCount 等于0。在进入此状态会会调用 terminated() 方法
  • TERMINATED = 3 << COUNT_BITS; // 终止状态, 在 terminated() 方法返回后,由 TIDYING 状态进入此状态。

状态转换图:

addWorker()

此方法用来创建新的线程添加到线程池

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        /* * 检查线程池状况, 确保此时可以添加新的线程, * 如果是runing,那么跳过if。 * 如果rs>=SHUTDOWN,同时不等于SHUTDOWN,即为SHUTDOWN以上的状态,那么不接受新线程。 * 如果rs>=SHUTDOWN,同时等于SHUTDOWN,同时first != null,那么拒绝新线程, * 如果为Empty,那么队列已空,不需要增加消耗线程,返回 false。 * */
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
        for (;;) {
            int wc = workerCountOf(c);
            /* * 判断线程池是否已满,如果线程数大于等于最大容量 CAPACITY 直接返回false * core 是一个boolean 参数,表明调用者想把此线程添加到哪个线程池 * 根据 core 的值判断要添加的线程池是否已满 **/
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //CAS 操作增加工作线程数
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            //CAS 操作失败, 再次检查状态重来一次
            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 {
        //创建新的线程
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            //获取到锁
            mainLock.lock();
            try {
                // 再次检查状态,因为状态可能在获取锁之前改变
                int rs = runStateOf(ctl.get());
                //确保当前线程池还接收新的线程
                //结合上面的线程状态知道:当状态值大于等于 SHUTDOWN 时 线程池就不再接收新的线程了
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                //线程添加成功后就可以启动线程准备执行了
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

从上面两段源码可以看到, 在添加新的线程进入线程池时,各种操作非常的严谨细致,往往需要多次检查状态,确保线程池的正确运行。

worker工作线程

线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会无限循环获取工作队列里的任务来执行。我们可以从Worker的runWorker方法里看到:

final void runWorker(Worker w) {
    // 获取当前线程
    Thread wt = Thread.currentThread();
    // 获取w的firstTask
    Runnable task = w.firstTask;
    // 设置w的firstTask为null
    w.firstTask = null;
    // 释放锁(设置state为0,允许中断)
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) { // 任务不为null或者阻塞队列还存在任务
            // 获取锁
            w.lock();
            /* * 这里的检查主要是确保线程池此时还能接收新的任务去执行, 如果不在接收新的任务 * 则应该中断当前线程 **/
            if ((runStateAtLeast(ctl.get(), STOP) ||  
                 (Thread.interrupted() &&  
                  runStateAtLeast(ctl.get(), STOP))) &&   
                  !wt.isInterrupted())    
                wt.interrupt(); 
            try {
                // 在执行之前调用钩子函数
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 运行给定的任务
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    // 执行完后调用钩子函数
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                // 增加给worker完成的任务数量
                w.completedTasks++;
                // 释放锁
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 处理完成后,调用钩子函数
        processWorkerExit(w, completedAbruptly);
    }
}

参考

https://www.cnblogs.com/javanoob/p/threadpool.html
http://ifeve.com/java-threadpool/
https://mp.weixin.qq.com/s/5dexEENTqJWXN_17c6Lz6A


版权声明:本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。
喜欢 (0)