Java并发 - 线程池

363 阅读11分钟

为什么使用线程池

如果不使用线程池,每一个任务都需要新开一个线程。我们可以通过创建固定数量的线程来进行任务的执行,从而避免的线程的创建和销毁会带来资源的消耗问题。

使用线程池的好处

  • 加快响应速度
  • 合理利用CPU和内存
  • 线程的统一管理

线程池的适用场景

  • 服务器接受到大量请求时,使用线程池技术是非常合适的,它可 以大大减少线程的创建和销毁次数,提高服务器的工作效率
  • 实际上,在开发中,如果需要创建5个以上的线程,那么就可以使 用线程池来管理

创建和停止线程池

线程池构造函数的参数

  • corePoolSize:指的是核心线程数:线程池在完成初始化后,默认 情况下,线程池中并没有任何线程,线程池会等待有任务到来时 再创建新线程去执行任务
  • maxPoolSize:线程池有可能会在核心线程数的基础上,额外增加一些线程,但 是这些新增加的线程数有一个上限

添加线程的规则

  1. 如果线程数小于corePoolSize ,即使其他工作线程处于 空闲状态,也会创建一个新线程来运行新任务。
  2. 如果线程数等于(或大于) corePoolSize但少于 maximumPoolSize , 则将任务放入队列。
  3. 如果队列已满,并且线程数小于maxPoolSize , 则创建 一个新线程来运行任务。
  4. 如果队列已满,并且线程数大于或等于maxPoolSize, 则拒绝该任务。

  • keepAliveTime:如果线程池当前的线程数多于corePoolSize ,那么如果多余的线程空闲时间超过keepAliveTime ,它们就会被终止

  • threadFactory:新的线程是由ThreadFactory创建的,默认使用Executors.defaultThreadFactory() , 创建出来的线程都在同一个线程组,拥有同样的NORM_ PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory ,那么就可以改变 线程名、线程组、优先级、是否是守护线程等。

通常情况下使用默认的ThreadFactory就可以,但是也可以通过自定义的线程工厂来定义线程的名称等。

workQueue:用于存储线程池当前处理不了的任务(当前线程池的线程数量不超过最大线程数)

三种常见的队列类型

  • 直接交接: SynchronousQueue(不存储任务,减少在队列中的中转)
  • 无界队列: LinkedBlockingQueue
  • 有界的队列: ArrayBlockingQueue

handler:当有界队列被填满之后,并且线程达到了最大线程数量,这时候就需要使用饱和策略 可以通过setRejectExecutionHandler来修改。

JDK提供了RejectExecutionHandler的四种实现方式,当然也可以自定义设置

  • AbortPolicy();//默认,队列满了丢任务抛出异常
  • DiscardPolicy();//队列满了丢任务不异常
  • DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
  • CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务

线程池应该手动创建还是自动创建

手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险。

根据实际的情况进行创建更符合生产场景。

JDK提供的线程池

线程池名称 使用的工作队列 核心线程数 最大线程数
FixedThreadPool LinkedBlockingQueue 自定义参数 与核心线程数相同
SingleThreadExecutor LinkedBlockingQueue 1 1
CachedThreadPool SynchronousQueue 0 Integer.MAX_VALUE
ScheduledThreadPool DelayedWorkQueue(根据任务的时间先后进行延迟) 自定义参数 Integer.MAX_VALUE
  • FixedThreadPool (固定线程数量的线程池):
    由于传进去的LinkedBlockingQueue是没有容量上限的 所以当请求数越来越多,并且无法及时处理完毕的时候, 也就是请求堆积的时候,会容易造成占用大量的内存,可 能会导致OOM。

  • SingleThreadExecutor(单线程的线程池,只会用唯一的工作线程来执行任务) 和上述的newFixedThreadPool的原理基本一样,只不过把线程数直接设置成了1 ,所以这也会导致 同样的问题,也就是当请求堆积的时候,可能会占用大量的内存。

  • CachedThreadPool(可缓存的线程池) 无界线程池,具有自动回收多余线程的功能。这里的弊端在于第二个参数maximumPoolSize被设置为了Integer.MAX_VALUE ,这可能会创建数量非常多的线程,甚至导致OOM。

  • ScheduledThreadPool(支持定时及周期性任务执行的线程池)

  • WorkStealingPool(工作窃取算法实现的线程池,JDK1.8加入) 和上面的几种线程池有很大的不同,这里面的任务是能够产生子任务的任务,例如:二叉树的遍历这种*递归的情形。此外,相对于上述的四种线程池这种线程池的线程之间是会合作的,如果有线程已经完成的自己的任务,会去拿其他未完成线程的任务队列(不是工作队列)的子任务来执行(所以任务队列的中子任务的执行顺序是不能保证的)。

正确创建线程池的方法

根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大,我们想给线程取什么名字等等。

  • CPU密集型(加密、计算hash等) : 最佳线程数为CPU核心 数的1-2倍左右。
  • 耗时IO型(读写数据库、文件、网络读写等) : 最佳线程数一 般会大于cpu核心数很多倍,以JVM线程监控显示繁忙情况为 依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:线程数=CPU核心数* ( 1+平均等待时间/平均工作时间)

停止线程池的正确方法

  • void shutdown() :

启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。

  • boolean isShutdown() :

如果此执行者已关闭,则返回 true 。

  • boolean isTerminated() :

如果所有任务在关闭后完成,则返回 true 。 和isShutdown()不同的是如果执行者关闭而任务没完成则 isShutdown()返回true而isTerminated()返回false

  • boolean awaitTermination(long timeout, TimeUnit unit)

阻止所有任务在关闭请求完成后执行,或发生超时,或当前线程中断,以先到者为准。 简单点理解就是测试在一段时间内线程池是不是完全停止。

  • void terminated() 执行程序已终止时调用方法。在ThreadPoolExecutor中没有具体实现

  • List<Runnable> shutdownNow() 尝试停止所有主动执行的任务,停止等待任务的处理,并返回正在等待执行的任务列表。


任务太多,如何拒绝

拒绝的时机

  1. 当Executor关闭时,提交新任务会被拒绝。

  2. 以及当Executor对最大线程和I作队列容量使用有限边界并且已经饱和时。

4中拒绝策略

  • AbortPolicy();//默认,队列满了丢任务抛出异常
  • DiscardPolicy();//队列满了丢任务不异常
  • DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
  • CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务
  • 自定义拒绝策略(例如,对任务进行持久化)

线程池原理和源码分析

线程池组成部分

  • 线程池管理器
  • 工作线程.
  • 任务列队
  • 任务接口( Task )

Executor、ExecutorService、AbstractExecutorService、ThreadPoolExecutor的关系

Executors是一个工具类,主要用于创建不同的ThreadPoolExecutor

线程池实现任务复用的原理

  • 相同的线程执行不同的任务

原理:由主线程循环获取任务,让线程池中的线程去执行

ThreadPoolExecutor.execute()源码

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            // 创建新线程执行任务,如果返回false,则加入阻塞队列
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 加入阻塞队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // Double Check,再次检查是否能够通过新线程创建成功
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 没有线程了
            else if (workerCountOf(recheck) == 0)
                // 可能是workQueue中仍有未执行完成的任务,创建没有初始任务的worker线程执行
                addWorker(null, false);
        }
        // 加入阻塞队列失败,先尝试加入启动一个新的线程执行该任务,调用RejectHandler中的方法进行处理
        else if (!addWorker(command, false))
            reject(command);
    }
    
    /**
     * 就上述的整体流程而言
     * 1.如果线程数量小于核心线程的数量,增加新的线程对任务进行处理
     * 2.超过了线程数量,尝试加入阻塞队列,这一步需要进行Double Check,以及消息队列中任务的清理(如果线程数为0的话)
     * 3.阻塞队列已满,尝试以非核心线程任务方法是增加Worker
     */

ThreadPoolExecutor.runWorker()源码

/**
     * runWorker()的运行流程
     *
     * 1.根据worker获取要执行的任务task,然后调用unlock()方法释放锁,
     *  这里释放锁的主要目的在于中断,因为在new Worker时,设置的state为-1,
     *  调用unlock()方法可以将state设置为0,这里主要原因就在于interruptWorkers()
     *  方法只有在state >= 0时才会执行;
     * 2.通过getTask()获取执行的任务,调用task.run()执行,当然在执行之前会调用
     *  worker.lock()上锁,执行之后调用worker.unlock()放锁;
     * 3.在任务执行前后,可以根据业务场景自定义beforeExecute() 和 afterExecute()方法,
     *  则两个方法在ThreadPoolExecutor中是空实现;
     * 4.如果线程执行完成,则会调用getTask()方法从阻塞队列中获取新任务,
     *  如果阻塞队列为空,则根据是否超时来判断是否需要阻塞;
     * 5.task == null或者抛出异常(beforeExecute()、task.run()、afterExecute()均有可能)
     *  导致worker线程终止,则调用processWorkerExit()方法处理worker退出流程。
     * @param w
     */
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        // 帮助GC?
        w.firstTask = null;
        //  释放锁,运行中断
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            // task当前Worker的任务 -- 线程复用核心
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                // 确保只有当线程是stoping时,才会被设置为中断,否则清楚中断标示
                // 如果线程池状态 >= STOP ,且当前线程没有设置中断状态,则wt.interrupt()
                // 如果线程池状态 < STOP,但是线程已经中断了,再次判断线程池是否 >= STOP,如果是 wt.interrupt()
                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 {
                        // 和beforeExecute同理
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    // 完成任务数量+1
                    w.completedTasks++;
                    // 解锁
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            // 始终会调用
            processWorkerExit(w, completedAbruptly);
        }
    }

线程池的状态

线程池中利用AtomicInteger变量ctl来存储线程池的状态

/**
     * 变量ctl定义为AtomicInteger ,其功能非常强大,记录了“线程池中的任务数量”和“线程池的状态”两个信息。
     * 共32位,其中高3位表示"线程池状态",低29位表示"线程池中的任务数量"。
     */
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    /**
     * 低29位,用于存储容量值
     */
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    /**
     * RUNNING:处于RUNNING状态的线程池能够接受新任务,以及对新添加的任务进行处理。
     *
     * SHUTDOWN:处于SHUTDOWN状态的线程池不可以接受新任务,但是可以对已添加的任务进行处理。
     *
     * STOP:处于STOP状态的线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
     *
     * TIDYING:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,
     * 若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
     *
     * TERMINATED:线程池彻底终止的状态。
     *
     * 新建,就绪,运行,阻塞,死亡 -- 线程的5状态
     */

    // runState is stored in the high-order bits
    /**
     * RUNNING            -- 对应的高3位值是111。
     */
    private static final int RUNNING    = -1 << COUNT_BITS;
    /**
     * SHUTDOWN       -- 对应的高3位值是000。
     */
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    /**
     * STOP                   -- 对应的高3位值是001。
     */
    private static final int STOP       =  1 << COUNT_BITS;
    /**
     * TIDYING              -- 对应的高3位值是010。
     */
    private static final int TIDYING    =  2 << COUNT_BITS;
    /**
     * TERMINATED     -- 对应的高3位值是011。
     */
    private static final int TERMINATED =  3 << COUNT_BITS;


实现线程池需要注意的地方

  • 避免任务堆积
  • 避免线程数过度增加
  • 排查线程泄漏

线程执行完毕却不能被回收,任务执行可能存在问题


-参考课程:慕课网 - 玩转Java并发工具,精通JUC,成为并发多面手