【连载 06】自定义线程池(下)

1.4.3 线程工厂

Java线程工厂(Thread Factory)是Java SDK中java.util.concurrent包里的一个接口,通常用于创建新线程,允许使用者定制线程的创建过程,包括不限于设置线程名称、设置优先级、设置线程组、统计线程信息等等。

ThreadFactory只有一个接口,参数类型java.lang.Runnable,返回值类型java.lang.Runnable,内容如下:

Thread newThread(Runnable r);

在之前的代码演示当中,为了打印当前线程的名字,用到了Thread.currentThread().getName()代码,打印的信息通常是pool-1-thread-1格式的,不知道各位是否会有疑惑,这个格式是怎么定义的?没错,格式就来源于Java线程池默认的线程工厂,方法路径是java.util.concurrent.Executors#defaultThreadFactory,返回DefaultThreadFactory类对象,其中构造方法如下:

        DefaultThreadFactory() {

            SecurityManager s = System.getSecurityManager();

            group = (s != null) ? s.getThreadGroup() :

                                  Thread.currentThread().getThreadGroup();

            namePrefix = "pool-" +

                          poolNumber.getAndIncrement() +

                         "-thread-";

        }

到这里,默认线程名字之谜就算解开了。让咱们一起看看它的接口方法实现:

        public Thread newThread(Runnable r) {

            Thread t = new Thread(group, r,

                                  namePrefix + threadNumber.getAndIncrement(),

                                  0);

            if (t.isDaemon())

                t.setDaemon(false);

            if (t.getPriority() != Thread.NORM_PRIORITY)

                t.setPriority(Thread.NORM_PRIORITY);

            return t;

        }

这段代码首先根据构造方法里面已经完成初始化的属性和参数创建了线程,然后设置非守护线程,又将线程优先级设置为java.lang.Thread#NORM_PRIORITY。

下面演示如何自定义线程工厂:

package org.funtester.performance.books.chapter01.section4;

import java.util.concurrent.*;

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadFactoryDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 5, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {// 创建线程池

            AtomicInteger index = new AtomicInteger(1);// 线程索引

            @Override

            public Thread newThread(Runnable r) {

                Thread thread = new Thread(r);// 创建线程

                thread.setName("FunTester-" + index.getAndIncrement());// 设置线程名称

                return thread;

            }

        });

        for (int i = 0; i < 3; i++) {

            executor.execute(() -> {// 提交任务

                try {

                    Thread.sleep(1000);// 休眠1秒,模拟任务执行时间

                } catch (InterruptedException e) {

                    throw new RuntimeException(e);

                }

                System.out.println(Thread.currentThread().getName() + " is running");// 输出线程名称

            });

        }

        executor.shutdown();// 关闭线程池

    }

}

在这个例子中,通过线程工厂自定义了线程的名字,让我们来看看控制台打印内容:

FunTester-2 is running

FunTester-1 is running

FunTester-3 is running

这一下可读性提升巨大,又不失优雅。

1.4.3 拒绝策略

拒绝策略在自定义线程池中作用发挥关键作用,通常对于初学者来说,使用默认策略是一种保险的方式。为了更好展示这四种策略在线程池无法接收改任务时如果工作,我写了下面这个演示代码:

package org.funtester.performance.books.chapter01.section4;

import org.apache.kafka.common.utils.ThreadUtils;

import java.util.concurrent.LinkedBlockingQueue;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicInteger;

/**

 * 自定义线程池拒绝策略示例

 */

public class RejectDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new ThreadPoolExecutor.AbortPolicy());// 自定义线程池

        AtomicInteger index = new AtomicInteger();// 索引,用于标识任务

        for (int i = 0; i < 3; i++) {

            int sequence = i;// 任务序号,用于标识任务,由于lambda表达式中的变量必须是final或者等效的,所以这里使用局部变量

            try {

                executor.execute(() -> {// 提交任务

                    try {

                        Thread.sleep(1000);// 模拟任务执行,睡眠1秒,避免任务过快执行完毕

                    } catch (InterruptedException e) {

                        throw new RuntimeException(e);

                    }

                    System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  任务" + sequence + "执行完成");// 打印任务执行完成信息

                });

                System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  任务" + sequence + "提交成功");// 打印任务提交成功信息

            } catch (Exception e) {

                System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  任务" + sequence + "被拒绝,异常信息:" + e.getMessage());// 打印任务被拒绝信息

            }

        }

        executor.shutdown();// 关闭线程池,不再接受新任务,但会执行完队列中的任务,并不会立即关闭

    }

}

当我们使用AbortPolicy策略时,控制台打印如下:

main  1712992116303  任务0提交成功

main  1712992116303  任务1提交成功

main  1712992116303  任务2被拒绝,异常信息:Task org.funtester.performance.books.chapter01.section3.RejectDemo$$Lambda/0x0000000301004200@2b71fc7e rejected from java.util.concurrent.ThreadPoolExecutor@2ef1e4fa[Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]

pool-1-thread-1  1712992117304  任务0执行完成

pool-1-thread-1  1712992118305  任务1执行完成

说明前两个任务都可以被正常提交且执行成功,第三个任务提交时被线程池拒绝了。因为前两个任务,一个先被线程池执行,一个待在等待队列中,第三任务提交时,已经没有空的位置了。

当我们使用DiscardPolicy拒绝策略时,控制台打印如下:

main  1712992169434  任务0提交成功

main  1712992169434  任务1提交成功

main  1712992169434  任务2提交成功

pool-1-thread-1  1712992170435  任务0执行完成

pool-1-thread-1  1712992171436  任务1执行完成

说明前两个任务被正常提交且正常执行,第三个任务虽然提交成功,但是未被执行。

当我们使用DiscardOldestPolicy拒绝策略时,控制台打印如下:

main  1712992249578  任务0提交成功

main  1712992249578  任务1提交成功

main  1712992249578  任务2提交成功

pool-1-thread-1  1712992250579  任务0执行完成

pool-1-thread-1  1712992251580  任务2执行完成

说明三个任务都被提交成功,但是当第三个任务提交时,第一个任务在执行中,第二个任务在等待队列中,此时线程池丢弃了等待队列中的任务(此处并不能证明丢弃的是第一个任务),然后将第三个任务留在了队列中等待执行。这一点可以从时间戳相差1秒左右看出,第三个任务还是交给线程池执行的,由于线程池只有一个线程,所以需要等待执行完第一个任务,线程变得空闲才会去执行第三个任务。

下面是java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy这部分源码:

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {

            if (!e.isShutdown()) {

                e.getQueue().poll();

                e.execute(r);

            }

        }

当我们选择CallerRunsPolicy拒绝策略时,控制台打印如下:

main  1712992627964  任务0提交成功

main  1712992627964  任务1提交成功

pool-1-thread-1  1712992628965  任务0执行完成

main  1712992628969  任务2执行完成

main  1712992628969  任务2提交成功

pool-1-thread-1  1712992629966  任务1执行完成

说明三个任务都提交成功(请注意这里提交成功仅仅表示提交方法没有报错),虽然都被执行了,但只有前两个任务是被线程池线程执行的,所以他们间隔约为1秒。而第三个任务实际是被main线程执行的,执行完成的时间和第一个任务几乎同时,还有第三个任务打印的日志是先完成,后提交,切两行日志同时打印。main线程在提交第三个任务时,由于被线程池拒绝,所以自己执行了第三个任务,这才导致日志先执行后提交。

下面是java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy这部分的源码:


        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {

            if (!e.isShutdown()) {

                r.run();

            }

        }

当我们选择自定义线程池时,要根据实际的需要选择合适的拒绝策略。例如我们开发一个异步任务线程池时,为了保障所有异步任务都能够执行,除了将等待任务队列设置合适大小以外,也可以通过使用CallerRunsPolicy拒绝策略,由当前线程执行异步任务。

但这样做带来一个问题,本来我们设置了最大线程数来限制最大并发,如果由提交任务的线程自己执行异步任务,会给被请求服务造成超出线程池设置的压力。此时我们需要重新自定义一个拒绝策略,如果提交任务失败,那么重新提交,当然这里需要一个等待间隔。自定义拒绝策略代码如下:

        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new RejectedExecutionHandler() {

            @Override

            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

                System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  任务被拒绝,线程池已满");// 打印任务被拒绝信息

                try {

                    Thread.sleep(1000);// 等待1秒,避免递归调用导致栈溢出

                } catch (InterruptedException e) {

                    throw new RuntimeException(e);

                }

                executor.execute(r);

            }

        });// 自定义线程池

使用自定义拒绝策略时,控制台打印如下:

main  1712993224466  任务0提交成功

main  1712993224466  任务1提交成功

main  1712993224466  任务被拒绝,线程池已满

pool-1-thread-1  1712993225467  任务0执行完成

main  1712993225468  任务2提交成功

pool-1-thread-1  1712993226469  任务1执行完成

pool-1-thread-1  1712993227470  任务2执行完成

可以看出所有的任务均提交成功且执行成功,而且都是被线程池线程执行。

为了实现既不丢弃任务,也不使用额外线程执行异步任务,除了在拒绝策略里面增加重试,我们还是可以直接在提交任务时,选择阻塞添加。这样既可以免去设置拒绝策略进行重试,也可以避免使用java.lang.Thread#sleep(long)方法。

直接往线程池提交任务常用的两个方法均不支持阻塞调用,只有等待队列有阻塞调用方法,所以我们需要绕过线程池的API,直接想等待队列中提交任务。具体调用代码如下:

package org.funtester.performance.books.chapter01.section4;

import java.util.concurrent.LinkedBlockingQueue;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;

/**

 * 阻塞队列添加任务示例

 */

public class BlockAddTask {

    public static void main(String[] args) throws InterruptedException {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2));// 创建线程池,核心线程数1,最大线程数2,线程空闲时间10秒,任务队列为链表阻塞队列

        executor.prestartCoreThread();// 预启动核心线程

        for (int i = 0; i < 4; i++) {

            int sequence = i;// 任务序号,用于标识任务,由于lambda表达式中的变量必须是final或者等效的,所以这里使用局部变量

            Runnable runnable = () -> {

                try {

                    Thread.sleep(1000);// 模拟任务执行,睡眠1秒,避免任务过快执行完毕

                } catch (InterruptedException e) {

                    throw new RuntimeException(e);

                }

                System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  任务" + sequence + "执行完成");// 打印任务执行完成信息

            };

            executor.getQueue().put(runnable);// 将任务放入队列

            System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  任务" + sequence + "提交成功");// 打印任务提交成功信息

        }

        executor.shutdown();// 关闭线程池,不再接受新任务,但会执行完队列中的任务,并不会立即关闭

    }

}

控制台输出如下:

main  1713010970715  任务0提交成功

main  1713010970715  任务1提交成功

main  1713010970715  任务2提交成功

pool-1-thread-1  1713010971720  任务0执行完成

main  1713010971720  任务3提交成功

pool-1-thread-1  1713010972725  任务1执行完成

pool-1-thread-1  1713010973726  任务2执行完成

pool-1-thread-1  1713010974731  任务3执行完成

可以明显看到第四个任务提交时,阻塞了约1秒,原因是第一个任务在核心线程中执行,第二个和第三个任务在等待队列中,此时添加第四个任务,自然会阻塞在java.util.concurrent.BlockingQueue#PUT方法,另外我们还可以使用超时阻塞的API提交任务,如下:

executor.getQueue().offer(runnable, 10, TimeUnit.SECONDS);// 将任务放入队列

不知你有没有注意到这次用例的变化,核心线程数不为零,且调用了java.util.concurrent.ThreadPoolExecutor#prestartCoreThread方法,预启动核心线程。因为我们往线程池的任务队列中添加任务时,并不会走线程创建的逻辑,所以当线程池没有线程时,自然也不会执行等待队列中的任务。

1.4.4 动态线程数量

我们已经将线程池创建三个对象参数拆解,可以说非常彻底。但对于线程池最重要的设定线程数相关的两个参数,并没有进行过多改动,全依靠Java线程池自己的创建和管理逻辑。

是这两个参数初始化之后无法修改吗?答案并不是,因为在性能测试实践当中,我们可以根据实际使用场景的并发量,预估一个线程池的范围,用来设置创建线程池的两个参数。但我们依然可以通过Java线程池的API修改这两个参数。

假设我们的需求是创建一个线程池,在任务等待队列不为空的时候,尽可能创建更多线程去执行任务;在等待队列为空的时候,尽可能少地保留活跃线程;同时我们还要求任务等待队列有较多的存储容量。如果要从前面的演示过的线程池中选择的话,我们会有两个选择:

(1)缓存线程池,但是无法满足等待队列有较多的存储容量。

(2)核心数+最大线程数线程池+LinkedBlockingQueue。这类线程池会面临两个矛盾:若核心线程数较低,则无法在等待队列不满的情况下创建超过核心线程数的活跃线程;若核心线程数较高,当等待队列为空时,无法回收这些活跃线程所占资源。

为了解决这个需求,我们需要在Java线程池基础上,增加动态调整线程池线程配置的功能,这个功能可以由java.util.concurrent.ThreadPoolExecutor#setCorePoolSize这个API来完成。看着名字就能猜到这个API是用来设置核心线程数和。示例代码如下:

        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));

        for (int i = 0; i < 4; i++) {

            executor.execute(() -> {// 提交任务

                try {

                    Thread.sleep(3000);

                } catch (InterruptedException e) {

                    throw new RuntimeException(e);

                }

                System.out.println(Thread.currentThread().getName() + "  Hello, FunTester!");

            });

        }

        System.out.println("设置前线程池中活跃线程数量:" + executor.getActiveCount());

        executor.setCorePoolSize(2);// 设置核心线程数

        System.out.println("设置后线程池中活跃线程数量:" + executor.getActiveCount());

        executor.shutdown();// 关闭线程池

控制台打印内容如下:

设置前线程池中活跃线程数量:1

设置后线程池中活跃线程数量:2

pool-2-thread-2  Hello, FunTester!

pool-2-thread-1  Hello, FunTester!

pool-2-thread-1  Hello, FunTester!

pool-2-thread-2  Hello, FunTester!

可以看到,线程池中已经有两个活跃线程在处理异步任务了。那新的问题来了,如果想修改最大线程数改怎么办?巧了,Java线程池刚好有这么一个API:java.util.concurrent.ThreadPoolExecutor#setMaximumPoolSize,使用语法跟设置核心线程数一致,此处就不再单独演示了。下面分享一个动态调整线程数的功能类,其中包括以下几点功能:

  • (1)当任务堆积,增加核心线程数(在最大线程数以内),依靠线程池创建线程逻辑,增加活跃线程数。
  • (2)当线程池空闲,减少核心线程数,依靠线程池回收策略,降低活跃线程数。
  • (3)当任务严重堆积,增加最大线程数(在安全值以下),并且依靠核心线程数调整逻辑增加活跃线程数。
  • (4)当任务不再严重堆积,减少最大线程数,但保证在安全值以上。
  • (5)在每一次添加任务时,执行动态调整线程池逻辑。
package org.funtester.performance.books.chapter01.section4;

import java.util.concurrent.LinkedBlockingQueue;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;

/**

 * 动态线程池演示代码

 */

public class DynamicThreadPool {

    /**

     * 全局线程池

     */

    public static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));

    /**

     * 封装线程池的execute方法

     * @param command 需要执行的任务

     */

    public static void execute(Runnable command) {

        dynamic();// 动态调整线程池

        executor.execute(command);// 提交任务

    }

    /**

     * 动态调整线程池,并不完美,仅供参考

     */

    public static void dynamic() {

        int size = executor.getQueue().size();

        if (size > 0) {// 如果队列中有任务

            increaseCorePoolSize();// 增加核心线程数

            if (size > 100) {

                increaseMaximumPoolSize();// 增加最大线程数

            } else {

                decreaseMaximumPoolSize();// 减少最大线程数

            }

        } else {

            decreaseCorePoolSize();// 减少核心线程数

        }

    }

    /**

     * 增加核心线程数,这里暂时不做线程安全处理

     */

    public static void increaseCorePoolSize() {

        int maximumPoolSize = executor.getMaximumPoolSize();// 获取最大线程数

        int corePoolSize = executor.getCorePoolSize();// 获取核心线程数

        if (corePoolSize < maximumPoolSize) {// 如果核心线程数小于最大线程数

            corePoolSize++;// 增加核心线程数

            executor.setCorePoolSize(corePoolSize);// 设置核心线程数

            System.out.println("增加核心线程数为:" + corePoolSize);

        }

    }

    /**

     * 减少核心线程数,这里暂时不做线程安全处理

     */

    public static void decreaseCorePoolSize() {

        int corePoolSize = executor.getCorePoolSize();// 获取核心线程数

        if (corePoolSize > 1) {// 如果核心线程数大于1

            corePoolSize--;// 减少核心线程数

            executor.setCorePoolSize(corePoolSize);// 设置核心线程数

            System.out.println("减少核心线程数为:" + corePoolSize);

        }

    }

    /**

     * 增加最大线程数,这里暂时不做线程安全处理

     */

    public static void increaseMaximumPoolSize() {

        int maximumPoolSize = executor.getMaximumPoolSize();// 获取最大线程数

        maximumPoolSize++;// 增加最大线程数

        if (maximumPoolSize > 128) {

            return;// 最大线程数不超过128

        }

        executor.setMaximumPoolSize(maximumPoolSize);// 设置最大线程数

        System.out.println("增加最大线程数为:" + maximumPoolSize);

    }

    /**

     * 减少最大线程数,这里暂时不做线程安全处理

     */

    public static void decreaseMaximumPoolSize() {

        int maximumPoolSize = executor.getMaximumPoolSize();// 获取最大线程数

        if (maximumPoolSize <= 16) {

            return;// 最大线程数不小于16

        }

        maximumPoolSize--;// 减少最大线程数

        executor.setMaximumPoolSize(maximumPoolSize);// 设置最大线程数

        System.out.println("减少最大线程数为:" + maximumPoolSize);

    }

}

这里动态调整方法并不完美,首先没有考虑线程安全的情况,这个可以使用下一章的知识解决。其次该方法只在执行任务时执行,假设一段时间并没有新的任务提交,我们预想的核心线程数降低并不会被执行。有了这些API加持,相信各位一定可以掌握自定义线程池应对各类场景。

线程调整时核心线程数和最大线程数的知识点:

  • (1)当我们设置的最大线程数小于核心线程数,会报错java.lang.IllegalArgumentException。
  • (2)当我们设置核心线程数大于最大线程数,不会报错,线程池会创建超过最大线程数的活跃线程。

1.5 总结

本章我们首先从并发和并行的概念入手,理解两者在实际执行时的差异。然后对于Java多线程实现的3种方式进行代码演示,然后快步进入Java线程池的重点学习。Java线程池分了两个部分:Java自带两种线程池实现和自定义线程池。两者循序渐进,由浅入深,最终完成了Java线程池构造方法参数及其含义的学习。然后演示环境,笔者重点讲解了线程池创建线程的逻辑图、创建自定义线程池三个重要的对象参数,在实践中加深对知识点的理解。最后演示了自定义参数对象核心逻辑实战,并且通过简单的案例帮助大家加强了对于自定义线程池源码的认识。

通过本章的学习,相信你已经掌握了Java多线程编程的核心要义,对于多线程有了更深的理解,对于线程池的使用也有了一定心得。这些都是我们使用Java性能测试的基础,学好基础可以让我们在未来的实际使用中举一反三。

书的名字:从 Java 开始做性能测试

如果本书内容对你有所帮助,希望各位多多赞赏,让我可以贴补家用。赞赏两位数可以提前阅读未公开章节。我也会尝试制作本书的视频教程,包括必要的答疑。

FunTester 原创精华
posted @ 2024-12-20 18:39  FunTester  阅读(4)  评论(0编辑  收藏  举报