Java并发编程(五)—线程池ThreadPoolExecutor详解

前言

在Java并发场景下,Java线程池是最经常运用的并发框架,而且在诸如Tomcat、数据库等工具中也都用到了Java线程池。合理地使用线程池可以给编程带来很多好处。

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

线程池的结构组成

线程池的结构主要指的是线程池的大小、任务队列、饱和策略。

  • 线程池的大小:corePoolSize(线程池的基本大小)是线程池中保持活动的最小线程数。maximumPoolSize(线程池最大数量)是线程池允许创建的最大线程数。
  • 任务队列:用于存储等待处理的任务,任务队列时阻塞队列。常用的任务队列有:ArrayBlockingQueue,一个基于数组结构的有界阻塞队列,按照FIFO原则对任务进行排序;LinkedBlockingQueue,一个基于链表结构的阻塞队列,按照FIFO原则对任务进行排序;SynchronousQueue,一个不存储任务的阻塞队列,每加入一个任务后需要等到任务被线程移出处理后才能加入下一个任务。
  • 线程工厂(ThreadFactory):线程工厂用于创建线程池中的线程。
  • 饱和策略(RejectedExecutionHandler):当线程池满了以后(线程达到最大线程数,任务队列也满的情况下)采用的任务处理策略。默认策略是AbortPolicy,直接抛出异常。除此之外还有,CallerRunsPolicy,只用调用者所在线程来运行任务;DiscardOldestPolicy,丢弃任务队列里最近的一个任务,并执行当前任务;DiscardPolicy,不处理,直接丢弃。
  • 线程活动保持时间(KeepAliveTome):线程池的工作线程空闲后,保持存活的时间。

线程池的工作流程

在程序中向线程池提交任务时,线程池就开始了工作。

  1. 向线程池提交一个新任务,判断核心线程池(corePoolSize个的线程)里的线程是否都在工作,如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里面的线程都在执行任务,进入下一步;
  2. 线程池工作队列是否已满。如果工作队列没有满,将任务加入到工作队列。如果工作队列已满,进入下一步。
  3. 再次判断线程池中的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果都在运行,执行饱和策略。

在这里插入图片描述

通过源码看ThreadPoolExecutor执行原理

我们从提交任务开始分析。使用ThreadPoolExecutor时调用execute()方法提交任务。在介绍执行原理的之前,线介绍ThreadPoolExecutor的一个重要属性ctl。这个属性是一个AtomicInteger类型的数据。它的主要作用是记录当前线程池中运行线程的数量以及线程池的状态。其中前4位记录运行状态,后28位记录线程最大数量。

private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1; //等于2^29-1
//判断线程池状态
 private static int runStateOf(int c)     { return c & ~COUNT_MASK; }
 
 /**
  * 状态有RUNNING、SHUTDOWN、STOP 、TIDYING、TERMINATED         
  */
 private static final int RUNNING    = -1 << COUNT_BITS;
 private static final int SHUTDOWN   =  0 << COUNT_BITS;
 private static final int STOP       =  1 << COUNT_BITS;
 private static final int TIDYING    =  2 << COUNT_BITS;
 private static final int TERMINATED =  3 << COUNT_BITS;
 //计算运行状态的线程数量
 private static int workerCountOf(int c)  { return c & COUNT_MASK; }
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. 如果正在运行少于corePoolSize数量的线程,则创建一个新的线程执行任务。
         *  然后调用addWorker以原子操作的方式检查(runState)线程工作状态和(workCount)工作线程数量。
         *  通过返回false来确定在此时不应该添加线程。
         *
         * 2.如果任务可以加入到队列中,那么我们仍然需要检查是否应该添加一个线程
         * (因为自上次检查后现有的线程可能已经死亡),
         *  或者自从进入此方法后池关闭了。 
         *  所以我们重新检查状态,如果必要的话,如果没有则回滚入队,或者如果没有,则启动新的线程。
         *
         * 3.如果工作队列已满,再次尝试添加一个新线程。 
         *  如果失败,执行饱和策略。
         */
        int c = ctl.get(); //获取线程数量状态位,通过workerCountOf()方法获取运行的线程数量
        //当前运行线程小于corePoolSize
        if (workerCountOf(c) < corePoolSize) {
        	//为true,新建线程执行任务
            if (addWorker(command, true))
                //任务加入线程池成功
                return;
            c = ctl.get();
        }
        //1.尝试加入到工作队列中 isRunning{c < SHUTDOWN}是判断线程池是否在运行
        //2.因为ctl.get()的初始值时 RUNNING | 0位负数(也只有RUNNING状态的值为负数)
        //3.将任务加入到工作队列中
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //再次尝试执行任务
        else if (!addWorker(command, false))
        	//失败则执行饱和策略
            reject(command);
    }
/**
 * 向线程池加入任务
 */
private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (int c = ctl.get();;) {
            // Check if queue empty only if necessary.
            //判断线程池状态
            if (runStateAtLeast(c, SHUTDOWN)  
                && (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                return false;

            for (;;) {
                if (workerCountOf(c)
                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                    return false;
                    //通过原子操作增加线程运行数量
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateAtLeast(c, SHUTDOWN))
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
        	//初始化新的Worker,包括新的工作线程
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int c = ctl.get();

                    if (isRunning(c) ||
                        (runStateLessThan(c, STOP) && 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;
    }

以上就是线程池执行任务的主要代码逻辑。

说完线程池的运行流程后,还有个细节就是线程池的keepAlive机制。我们知道,线程池有个重要的参数是keepAliveTime,它的含义是规定时间内,如果当前运行的线程数量大于corepoolSize时,空闲线程在keepAliveTime的时间内没取到任务时就结束线程。

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
           //获取task
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    try {
                        task.run();
                        afterExecute(task, null);
                    } catch (Throwable ex) {
                        afterExecute(task, ex);
                        throw ex;
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            //线程退出
            processWorkerExit(w, completedAbruptly);
        }
    }

上面的代码我们可以看到在获取不到task时线程就会退出。其中task是提交是的任务,这个任务执行完以后,就开始从getTask()方法中获取。我们看下getTask()方法。

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
          //......
          // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : //keepAliveTime时间内获取不到就返回null
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

上面的代码不难理解,在工作队列中,通过poll()方法获取task,如果keepAliveTime时间内获取不到就继续循环获取任务,在接下来的循环中,timeOut设置为true,可以看到如果工作队列没有任务排队时,会返回null。这个时候while循环结束,会执行processWorkerExit()方法清除线程。

线程池的使用

《阿里Java开发规范》中提到的三条建议

首先说一下《阿里Java开发规范》中提到的三条建议。

  1. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
  2. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
  3. 线程池不允许使用 ExecutorsExecutors去创建,而是通过ThreadPoolExecutor去创建,这样的处理方式让写同学更加明确线程池运行规则,避资源耗尽风险。
    说明: Executors返回的线程池对象的弊端 如下 :
    1)FixedThreadPool和 SingleThreadPoolPool: 允许的请求队列长度为 Integer.MAX_VALUE,可 能会堆积大量的请求,从而导致 OOM。
    2)CachedThreadPool 和 ScheduledThreadPool : 允许的创建线程数量 为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
线程池的主要参数配置

《阿里Java开发规范》关于线程池的使用主要是强调通过更多参数的设定使线程池有更强的操作性和可管理性。那么线程池ThreadPoolExecutor都有哪些主要的参数呢?

  • corePoolSize(线程池的基本大小)是线程池中保持活动的最小线程数。
  • 任务队列:用于存储等待处理的任务,任务队列时阻塞队列。常用的任务队列有:ArrayBlockingQueue,一个基于数组结构的有界阻塞队列,按照FIFO原则对任务进行排序;LinkedBlockingQueue,一个基于链表结构的阻塞队列,按照FIFO原则对任务进行排序;SynchronousQueue,一个不存储任务的阻塞队列,每加入一个任务后需要等到任务被线程移出处理后才能加入下一个任务。建议使用有界队列,可以增加稳定性,无界队列在异常情况下可能会导致无限的加入任务使程序崩溃
  • 线程工厂(ThreadFactory)用于创建线程池中的线程,可以确定线程的属性名称等。
  • 饱和策略(RejectedExecutionHandler,当线程池满了以后(线程达到最大线程数,任务队列也满的情况下)采用的任务处理策略。默认策略是AbortPolicy,直接抛出异常。除此之外还有,CallerRunsPolicy,只用调用者所在线程来运行任务;DiscardOldestPolicy,丢弃任务队列里最近的一个任务,并执行当前任务;DiscardPolicy,不处理,直接丢弃。
  • 线程活动保持时间(KeepAliveTome):线程池的工作线程空闲后,保持存活的时间。如果任务很多,并且任务执行的时间较短,可以调大时间,提高线程的利用率。
合理的配置线程池
要合理的配置线程池,可以根据以下几方面来分析:
  • 任务的性质:CPU密集型任务、IO密集型任务、混合任务
  • 任务的优先级:高、中、低等
  • 任务的执行时间:长、短
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接

一点建议:比如CPU密集型任务则需要配置较少的线程。因为任务会使用大量CPU资源,过多的线程可能会造成CPU压力过大,导致线程切换频繁,从而效率变低。一般配置N+1个线程(N是CPU的数量)。IO密集型任务会有大量时间等待IO操作,所以可能多配置一些线程。比如2N个线程(N是CPU的数量)。

总结

线程池在并发编程中是很重要的角色。合理的使用线程池可以提高程序运行效率,并且帮助我们方便的管理和监控线程的运行。所以了解线程池的运行原理是必须的。这里主要介绍了线程池的执行流程并且从源码角度分析了运行原理。

参考资料:《Java并发编程的艺术》、《Java编程思想》、《阿里Java开发规范》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值