Detailed five kinds of Java thread pool, thread management more elegant

Bloggers statement:

Please reprint this article at the beginning of additional links and author information, and marked as reserved. This article by the blogger  Whiskers Meow  original audience for support and advice.

This article first appeared in this    blogger : Whiskers Meow   |   blog home page : https://blog.csdn.net/smile_running

    In application development, there is usually such a demand is concurrent download file operations, such as Baidu network disk to download files, download video, Tencent video, you can download several files at once, which is concurrent downloads. Concurrent download process must be multithreaded operations, and create a large number of threads, it is bound to affect the performance of the program, resulting in Caton and other issues. So, Java provides us with the thread pool to manage threads .

    First, let's look at what the thread pool is? As the name suggests, like a thread storage pond, we can think of the pool. Means that you can store thread pool thread, and the thread pool to allow reuse, if one thread of execution over the pool and not directly destroy its life, can survive for some time, until the time of the next task, it We will reuse this thread in waiting, to avoid the overhead of creating a thread again.

 Baidu introduction to the thread pool:

[Thread pool (in English: thread pool): one kind of thread usage patterns. Thread scheduling overhead will bring too much, thereby affecting the overall performance and cache locality. The thread pool maintains multiple threads, waiting for the supervision of managers assign tasks can be executed concurrently. This avoids the cost of creating and destroying threads in dealing with short-term task. Thread pool not only to ensure the full utilization of the kernel, but also to prevent over-scheduling. The number of threads should be used depends on the number of concurrent processors, processor cores, memory, network sockets, etc. available. For example, the number of threads and generally suitable number of cpu +2, the number of threads results in additional excess thread switching overhead. ]

    The concept and role of the thread pool will introduce over, the following is the use of the thread pool, and we look at an example like this, simulate network download function, open the multitasking download operation, where each download will open up a new thread to execute.

Renderings:

    You can see that this effect, where per-click download  button will open a sub-thread to update the progress of the operation. Note: the names we see here is the name of the thread. It is observed that 5 download task thread used is different, so their names are not the same thread.

    In other words, each of us is a mission to open up new thread, when we download task if the amount is very large, it will not be open thread control, it will not speak a performance problem, if there is a security thread or threads scheduling to deal with them is very difficult. So in this case, is very necessary to introduce our thread pool to manage these threads, we have just introduced the advantages of the thread pool, now let's look at the specific implementation, in order to appreciate it in the end there are those advantages.

    First, our thread pool type of a total of four kinds, namely newSingleThreadPool, newFixedThreadPool, newCachedThreadPool, newScheduledThreadPool four types this is JDK1.8 version before, in JDK1.8 version joined a: newWorkStealingPool, so now total five species.

1, the process of creating a thread pool

    This thread pool by several names, we can roughly guessed it was intended, of course, still have to practice it. For  the thread pool are generally created such a step:

        //创建单核心的线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //创建固定核心数的线程池,这里核心数 = 2
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        //创建一个按照计划规定执行的线程池,这里核心数 = 2
        ExecutorService executorService = Executors.newScheduledThreadPool(2);
        //创建一个自动增长的线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //创建一个具有抢占式操作的线程池
        ExecutorService executorService = Executors.newWorkStealingPool();

    We just need to create such a call can be successfully applied to our thread pool, but not on top of things come from above, we have to enter the builder thread pool is created, the code is as follows:

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters and default thread factory and rejected execution handler.
     * It may be more convenient to use one of the {@link Executors} factory
     * methods instead of this general purpose constructor.
     *
     * @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.
     * @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} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

    Of course, the above comments are all the parameters are introduced, we use our own language to summarize:

  1. corePoolSize : 表示线程池核心线程数,当初始化线程池时,会创建核心线程进入等待状态,即使它是空闲的,核心线程也不会被摧毁,从而降低了任务一来时要创建新线程的时间和性能开销。
  2. maximumPoolSize : 表示最大线程数,意味着核心线程数都被用完了,那只能重新创建新的线程来执行任务,但是前提是不能超过最大线程数量,否则该任务只能进入阻塞队列进行排队等候,直到有线程空闲了,才能继续执行任务。
  3. keepAliveTime : 表示线程存活时间,除了核心线程外,那些被新创建出来的线程可以存活多久。意味着,这些新的线程一但完成任务,而后面都是空闲状态时,就会在一定时间后被摧毁。
  4. unit : 存活时间单位,没什么好解释的,一看就懂。
  5. workQueue : 表示任务的阻塞队列,由于任务可能会有很多,而线程就那么几个,所以那么还未被执行的任务就进入队列中排队,队列我们知道是 FIFO 的,等到线程空闲了,就以这种方式取出任务。这个一般不需要我们去实现。

还有一个注意点就是它这里的规定,可能会抛出这样的异常情况。这下面写的很明白了,就不要再介绍了:

* @throws IllegalArgumentException if one of the following holds:<br>
*         {@code corePoolSize < 0 }
*         {@code keepAliveTime < 0 }
*         {@code maximumPoolSize <= 0 }
*         {@code maximumPoolSize < corePoolSize }
* @throws NullPointerException if {@code workQueue} is null

    好了,以上重点几个参数内容我们介绍完了,现在来看看几种线程池的比较和表现吧!

2、线程池的比较

(1)newSingleThreadPool,为单核心线程池,最大线程也只有一个,这里的时间为 0 意味着无限的生命,就不会被摧毁了。它的创建方式源码如下:

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

    最形象的就是拿我们下载那个例子,为了便于测试,我当然添加了一个 全部下载的功能, newSingleThreadPool 测试结果如下:

    由于我们的线程池中使用的从始至终都是单个线程,所以这里的线程名字都是相同的,而且下载任务都是一个一个的来,直到有空闲线程时,才会继续执行任务,否则都是等待状态。

(2)newFixedThreadPool,我们需要传入一个固定的核心线程数,并且核心线程数等于最大线程数,而且它们的线程数存活时间都是无限的,看它的创建方式:

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

    对比 newSingleThreadPool,其实改变的也就是可以根据我们来自定义线程数的操作,比较相似。我们通过newFixedThreadPool(2)给它传入了 2 个核心线程数,看看下载效果如何:

    显然,它就可以做到并发的下载,我们两个下载任务可以同时进行,并且所用的线程始终都只有两个,因为它的最大线程数等于核心线程数,不会再去创建新的线程了,所以这个方式也可以,但最好还是运用下面一种线程池。

(3)newCachedThreadPool,可以进行缓存的线程池,意味着它的线程数是最大的,无限的。但是核心线程数为 0,这没关系。这里要考虑线程的摧毁,因为不能够无限的创建新的线程,所以在一定时间内要摧毁空闲的线程。看看创建的源码:

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

    没有核心线程数,但是我们的最大线程数没有限制,所以一点全部开始下载,就会创建出 5 条新的线程同时执行任务,从上图的例子看出,每天线程都不一样。看不出这个线程池的效果,下面我们通过修改这个逻辑。

    首先,我们点开始下载,只会下载前面三个,为了证明线程的复用效果,我这里又添加了一个按钮,在这个按钮中继续添加后面两个下载任务。

那么,当线程下载完毕时,空闲线程就会复用,结果显示如下,复用线程池的空闲线程:

另一种情况,当线程池中没有空闲线程时,这时又加了新的任务,它就会创建出新的线程来执行任务,结果如下:

    这下算是搞清楚这种线程池的作用了吧,但是由于这种线程池创建时初始化的都是无界的值,一个是最大线程数,一个是任务的阻塞队列,都没有设置它的界限,这可能会出现问题。

这里可以参考我的一篇文章: AsyncTask 源码 分析,或者这个 单利模式 解读的文章,里面有提到如何创建自定义的线程池,参考的是 AsyncTask 的源码线程池创建代码。

(4)newScheduledThreadPool,这个表示的是有计划性的线程池,就是在给定的延迟之后运行,或周期性地执行。很好理解,大家应该用过 Timer 定时器类吧,这两个差不多的意思。它的构造函数如下:

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

内部有一个延时的阻塞队列来维护任务的进行,延时也就是在这里进行的。我们把创建 newScheduledThreadPool 的代码放出来,这样对比效果图的话,显得更加直观。

        //参数2:延时的时长
        scheduledExecutorService.schedule(th_all_1, 3000, TimeUnit.MILLISECONDS);
        scheduledExecutorService.schedule(th_all_2, 2000, TimeUnit.MILLISECONDS);
        scheduledExecutorService.schedule(th_all_3, 1000, TimeUnit.MILLISECONDS);
        scheduledExecutorService.schedule(th_all_4, 1500, TimeUnit.MILLISECONDS);
        scheduledExecutorService.schedule(th_all_5, 500, TimeUnit.MILLISECONDS);

这个线程池好像不是很常用,做个了解就好了。

(5)newWorkStealingPool,这个是 JDK1.8 版本加入的一种线程池,stealing 翻译为抢断、窃取的意思,它实现的一个线程池和上面4种都不一样,用的是 ForkJoinPool 类,构造函数代码如下:

    /**
     * Creates a thread pool that maintains enough threads to support
     * the given parallelism level, and may use multiple queues to
     * reduce contention. The parallelism level corresponds to the
     * maximum number of threads actively engaged in, or available to
     * engage in, task processing. The actual number of threads may
     * grow and shrink dynamically. A work-stealing pool makes no
     * guarantees about the order in which submitted tasks are
     * executed.
     *
     * @param parallelism the targeted parallelism level
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code parallelism <= 0}
     * @since 1.8
     */
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    从上面代码的介绍,最明显的用意就是它是一个并行的线程池,参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别,前面4种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。从介绍中,还说明这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作。

如下图,任务的执行是无序的,哪个线程抢到任务,就由它执行:

    对比了以上 5 种线程池,我们看到每个线程池都有自己的特点,这也是为我们封装好的一些比较常用的线程池。当然,我建议你在使用(3)可缓存的线程池时,尽量的不要用默认的那个来创建,因为默认值都是无界的,可能会出现一些问题,这时我们可以参考源码中的线程池初始化参数的设置,可以尽可能的避免错误发生。

    通过这个案例,我们把线程池学习了一遍,总结一下线程池在哪些地方用到,比如网络请求、下载、I/O操作等多线程场景,我们可以引入线程池,一个对性能有提升,另一个就是可以让管理线程变得更简单。

相关博文推荐:你确定不看看这篇线程文章吗?

>>最全的 Java 线程知识点及案例

发布了101 篇原创文章 · 获赞 766 · 访问量 87万+

Guess you like

Origin blog.csdn.net/smile_Running/article/details/91409942