java中的线程池知识点整理

153 阅读7分钟

线程池的好处

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

线程池的7个构造参数

  1. corePoolSize(线程池的基本大小) 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即时其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建 2. maximumPoolSize(线程池最大数量) 线程池允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用无界阻塞队列做任务队列,则这个参数没有什么效果 3. keepAliveTime(线程活动保持时间) 线程池的工作线程空闲后,保持存活的时间。如果任务很多,并且每个任务的执行时间都比较短,可以调大时间,提高线程利用率 4. unit(线程保持活动时间的单位) 可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微妙(MICROSECONDS)和纳秒(NANOSECONDS) 5. workQueue(任务队列) 用于保存等到执行的任务的阻塞队列,可以选择以下几个阻塞队列:
    • ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序
    • LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常高于 ArrayBlockingQueue,并且没有边界
    • SynchronousQueue 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一致处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue
    • PriorityBlockingQueue 一个具有优先级的无界阻塞队列。

6. threadFactory 用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字 7. 拒绝策略 当任务和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理提交的新任务。在 JDK5 中线程池框架提供了以下四种策略: 也可以根据需要实现 RejectedExecutionHandler 接口自定义策略

  • AbortPolicy:直接抛出异常,默认采取这种策略
  • CallerRunsPolicy:使用调用者所在线程来运行任务
  • DiscardOldestPolicy:丢弃队列最近的一个任务,并执行当前任务
  • DiscardPolicy:不处理,丢弃掉

线程池提交任务流程

  1. 任务提交后,线程池会判断和核心程数是否已经满了,如果没有满,则创建线程执行任务。
  2. 如果核心线程数已经满了,那么判断任务队列是否满了,如果任务队列没有满,则将任务添加到任务队列
  3. 如果任务队列已经满了,那么判断当前线程池中线程数是否小于最大线程数,如果小于最大线程数,那么创建线程执行任务。
  4. 如果线程数已经已经等于最大线程数,那么该任务不能被线程池处理,执行对应的拒绝策略。

线程池生命周期

  1. 线程池正常运行时,调用shutdown方法,线程池处于shutdown状态,此时,线程池不再接受新的任务,会继续处理任务队列中的任务和线程正在处理的任务。
  2. 如果直接调用shutdownNow方法,线程池不仅不会接受新的任务,并且当前执行的任务和任务队列中的任务都会停止执行,此时线程池处于stop状态,并返回没有执行的任务列表。
  3. 线程池中任务都停止了,工作线程也全部结束,即tinying状态。
  4. terminated状态,线程池已经销毁。

线程池是如何实现线程复用的?

我们都知道,正常情况下,线程执行完 run方法,线程就需要被java虚拟机回收,那么为什么线程池中的线程是怎么实现复用的呢? 先说答案,其实很简单,就是不让run方法执行完,通过死循环加阻塞的形式,对线程实现复用。

线程执行java任务核心代码如下:

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            // 不断尝试从任务队列getTask()方法获取任务,如果没有获取到任务,阻塞当前线程
            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);
                    Throwable thrown = null;
                    try {
                        // 获取到任务的线程执行任务的run方法
                        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;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

线程池中,将每个线程包装成了Worker对象,HashSet中,worker会不断从阻塞队列中尝试获取任务,如果获取成功,执行任务,如果没有获取到任务,阻塞队列会阻塞当前线程,将当前线程挂起。等待阻塞队列中有任务了,再次将线程唤醒,(:这个模式是不是跟我们学习的生产者消费者模式像的一批?)

JDK提供的5种线程池

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

核心线程数和最大线程数都是1,阻塞队列是一个无界的LinkedBlockingQueue。该线程池适用于任务顺序执行,缺点但是不能充分利用CPU多核性能 2. newFixedThreadPool

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

核心线程数和最大线程数是相等的,阻塞队列也是无界的LinkedBlockingQueue,该线程池适用于能估算出需要多少核心线程数量的场景 3. newCachedThreadPool

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

核心线程数为0,最大线程数为int最大值,可以理解为无穷大,线程没有任务执行存活时间为60S,阻塞队列是SynchronousQueue。这种阻塞队列没有容量,功能相当于转发任务,该线程池任务来临时会不断的创建新的worker线程,所以,比较适合执行大量,轻量级任务 4. newScheduledThreadPool

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

该线程池的阻塞队列是一个延迟队列,该延迟队列内部维护了一个大根堆,可以对任务进行排序,该线程池适合执行定时任务或者延迟任务。 5. newWorkStealingPool

    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

这个是JDK1.8新出的一个线程池,内部使用了ForkJoin技术,可以对任务进行拆分,然后多个线程并行执行,然后将结果进行汇聚,类似于算法中的分治思想。

线程池线程数设置

方式1 - 经验之谈

一般线程池执行的任务可以分为两种,一种是IO密集型,一种是CPU密集型。

  • IO密集型:CPU核心数*2 因为IO不占CPU的资源,可以多开启一些线程
  • CPU密集型:CPU核心数 + 1 生产环境中,很难通过计算获得具体的线程数设置,因为干扰因素有很多,详见:mp.weixin.qq.com/s/G0toGUjKQ…
方式2 - 任务量化

可以精确算出核心线程数,最大线程数,阻塞队列大小等信息。我没有看明白,感觉没有把CPU核数考虑进去,自己认为不太对,大家感兴趣的可以看一下www.cnblogs.com/waytobestco…

附录&参考文档