高级并发编程系列二(线程池:核心要点)

162 阅读7分钟

1.思考

在上一篇文章中,我们分享了为什么需要线程池,你都还记得吗?线程池的核心思想就是,复用线程资源,通过有效利用有限的资源来处理无限的任务

现在我们在项目中,实现并发编程能力的时候,大多数时候都是直接使用juc包中提供的线程池:ThreadPoolExecutor。那么你有想过吗?如果要我们自己实现一个线程池,你需要从什么地方开始着手,或者说需要考虑些什么问题呢?

让我们一起来尝试思考以下问题:

  • 线程池中应该会放多个线程,那么到底需要放多少呢?
  • 如果线程池中的线程少了,需要处理的任务多了,线程数不够该如何处理呢?
  • 如果线程池中的线程多了,需要处理的任务少了,多余的线程该如何处理呢?
  • 如果线程池中的线程都在忙着处理任务,且达到了最大线程数,新来的任务来了该如何处理呢?
  • 如果线程池中的线程数达到了最大,任务队列中的放满了任务,新来的任务该如何处理呢?

如果以上问题,你都有了答案,那么我想你应该能够很好的理解线程池,并在项目中很好的应用了,那就让我们开始吧

#考考你:
1.你知道设计实现一个线程池,有哪些核心要点需要考虑吗

2.案例

2.1.线程池核心要点

在考考你中,为了理解线程池,我们一起尝试提了一些问题,现在让我们一起来尝试回答这些问题。

线程池中应该会放多个线程,那么到底需要放多少呢?

我们知道多线程是为了提升应用的并发处理能力,但是我们还需要知道,计算机世界中的并发,其实是cpu骗人的把戏,并不存在真正的并发(这里我们假设是单核cpu)。

那么并发到底是怎么一回事呢?它其实是一个时间分片的机制。我们可以这么去理解,假设有两个小伙子A和B,都想要讨好美女C,那C一会理A就跟A约会,一会理B就跟B约会。让我们大家误以为A一直都在约会,B也一直都在约会。

那么你可能会说,既然是时间分片机制,同一时刻还是只能处理一个任务,没有真正的并发,还把编程模型复杂化了,对吧。

这个问题我们做一个场景假设,有两个读取文件的任务,对于计算机来说,读取文件是一项比较重的任务,涉及到IO操作,通常会发生IO阻塞。

现在A任务向cpu说:我要读取一个文件,cpu回答:你去读吧。于是A任务顺利去读取它想要的文件内容了。可是这个时候,文件内容并没有准备好,文件系统对A说:你得等一会儿。注意这里的等一会儿

然后B任务来了,跟cpu说,我要读取一个文件。cpu看了看A任务,A任务正在等待文件系统准备文件内容,等待的这段时间,啥也干不了。于是cpu心想不如赚点外快,告诉B任务:那你也来吧。

这个时候我们会看到,整体时间没变,但是A任务,和B任务都得到了cpu处理,这就是我们需要理解的并发

好了,以上说了这么多,就是为了方便我们理解真正的并发,其实是cpu利用时间分片机制,在不同的任务之间不停的来回切换。这有助于我们在实际项目中,给线程池设置合适的线程数目,而不是简单的越多越好,也不是越少越好,根据经验大概是这样的:

  • 如果你们项目的任务类型,是cpu密集型的,比如加密,那么可以考虑设置为:cpu核心数的1-2倍
  • 如果你们项目的任务类型,是IO密集型的,比如访问数据库,那么可以考虑设置为:cpu核心数 * (1 + 任务平均等待时间/任务处理时间)
  • 当然最终的线程数设置多少合适,需要经过压测来确定

如果线程池中的线程少了,需要处理的任务多了,线程数不够该如何处理呢?

如果线程池中的线程多了,需要处理的任务少了,多余的线程该如何处理呢?

如果线程池中的线程数少了,比如:5个线程,需要处理的任务多了,比如:10个任务;这个时候为了提升应用的处理能力,且服务器资源充足,我们可以考虑:增加线程的数量,比如增加到10个线程

如果线程池中的线程多了,比如:10个线程,需要处理的任务少了,比如:5个任务;这个时候不需要那么多的线程,为了不浪费服务器资源,我们可以考虑:减少线程的数量,比如说减少到5个线程,让线程池保留5个线程就可以了

问:

如果线程池中的线程都在忙着处理任务,且达到了最大线程数,新来的任务来了该如何处理呢?

如果线程池中的线程数达到了最大,任务队列中的放满了任务,新来的任务该如何处理呢?

答:

如果线程池中的线程都在忙着处理任务,又来了新的任务,并且这个时候我们的线程数达到了最大,即不能再创建新的线程。那么我们可以考虑:将新来的任务,找个地方放着,稍安勿躁耐心等待一会儿,等有线程空出来了,再来处理这些等待的任务

如果线程池中线程数达到了最大,且任务队列中放满了任务,即安置的地方也放不下了。那么我们可以考虑:说NO,拒绝掉新来的任务,至于拒绝的策略,比如说报异常抗议,比如说直接丢弃。更多的拒绝策略,juc包中给我们提供了很好的参考策略,这里我们就不展开了

2.2.ThreadPoolExecutor源码参考

为了更好的理解我们在上面讨论的线程池核心要点,我们一起来看一下juc包提供的线程池类:ThreadPoolExecutor。看看一个设计实现良好的线程池工具,是否满足我们讨论的核心要点。

2.2.1.ThreadPoolExecutor构造方法

 /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the pool
     * @param keepAliveTime when the number of threads is greater than
     *  the core, this is the maximum time that excess idle threads
     *  will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     * executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    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;
    }

我把ThreadPoolExecutor线程池工具类,比较全的构造方法源码搬了过来,并且我想推荐你看一下相关的注释文档说明,有助于我们更好的理解线程池,形成一个良好的学习习惯

image.png

2.2.2.参数表格整理

image.png