【速看】一招教你快速如何快速确定线程池的线程数

线程池标准创建方式是通过标准构造器ThreadPoolExecutor去构造工作线程池。 构造器ThreadPoolExecutor的代码如下:

   public ThreadPoolExecutor(int corePoolSize, //核心线程数,即使线程空闲(Idle),也不会回收
                              int maximumPoolSize, //线程数的上限
                              long keepAliveTime,
                              TimeUnit unit, //线程最大空闲时长
                              BlockingQueue workQueue, //任务的排队队列
                              ThreadFactory threadFactory, //新线程的产生方式
                              RejectedExecutionHandler handler //拒绝策略) 

那么corePoolSize和maximumPoolSize 的该如何设置呢?在设置这两个值之前,首先需要通过任务类型对线程池进行分类, 可以分为IO密集型任务,CPU 密集型任务和混合型任务。

IO 密集型任务

此类任务主要是执行IO操作,由于执行IO操作的时间较长,导致CPU的利用率不高,这类任务CPU 常处于空闲状态。Netty就是典型的例子。

【速看】一招教你快速如何快速确定线程池的线程数_第1张图片

由于IO密集型任务的CPU使用率较低,导致线程的CPU空余时间比较多,因此线程数通常需要开CPU核数两倍的线程。当IO线程空闲时,可以启用其他线程继续使用CPU,以提高CPU的使用率。Netty的Reactor实现类的IO处理线程数默认正好为CPU核数的两倍。

IO密集型任务线程池演示代码如下:

/懒汉式单例创建线程池:用于IO密集型任务
public class IoIntenseTargetThreadPoolLazyHolder {
 /**
     * 有界队列size
     */
    public static final int QUEUE_SIZE = 128;
    //线程池: 用于IO密集型任务
    public static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
            IO_MAX,  //CPU核数*2
            IO_MAX, //CPU核数*2
            KEEP_ALIVE_SECONDS,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue(QUEUE_SIZE),
            new ThreadUtil.CustomThreadFactory("io"));

    public static ThreadPoolExecutor getInnerExecutor() {
        return EXECUTOR;
    }

    static {
        log.info("线程池已经初始化");

        EXECUTOR.allowCoreThreadTimeOut(true);
        //JVM关闭时的钩子函数
        Runtime.getRuntime().addShutdownHook(
                new ShutdownHookThread("IO密集型任务线程池", new Callable() {
                    @Override
                    public Void call() throws Exception {
                        //优雅关闭线程池
                        shutdownThreadPoolGracefully(EXECUTOR);
                        return null;
                    }
                }));
    }
}

测试代码如下:

 @Test
    public void testIoIntenseTargetThreadPool() {
        ThreadPoolExecutor pool = IoIntenseTargetThreadPoolLazyHolder.getInnerExecutor();
        ;
        for (int i = 0; i < 2; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    for (int j = 1; j < 10; j++) {
                        log.info(getCurThreadName() + ", 轮次:" + j);
                    }
                    log.info(getCurThreadName() + " 运行结束.");
                }
            });

        }
        ThreadUtil.sleepMilliSeconds(1000);
    }

}

在IO密集型线程池有以下几个要点:
(1)允许核心线程池销毁
IO线程池调用了allowCoreThreadTimeOut(…)方法,并传入了参数true,则keepAliveTime参数所设置的Idle 超时策略也将被应用与核心线程,当池中的线程长时间空闲时,可以自行销毁。

(2) 使用有界队列
使用有界队列而不是无界队列缓冲任务,如果128太小可以根据具体需要进行增大,但是不能使用无界队列。
【速看】一招教你快速如何快速确定线程池的线程数_第2张图片
(3)优先创建线程,而不是优先加入队列
corePoolSize、maximumPoolSize 保持一致,使得在接收到新任务时,如果没有空闲工作线程,则优先创建线程去执行线程,而不是优先加入阻塞队列,等待现有工作线程空闲后再执行。

(4) 使用内部静态类懒汉式单例模式创建线程池
使用懒汉式单例模式创建线程池,如果代码没有用到此线程,也不会立即出创建,只有在getInstance()被调用时才去加载内部类并且初始化单例,该方式解决了线程安全问题,也解决了写法繁琐问题。

(5)使用JVM关闭时的钩子函数,优雅的自动关闭线程池
【速看】一招教你快速如何快速确定线程池的线程数_第3张图片

CPU 密集型任务

此类任务主要是执行计算任务,由于响应时间很快,CPU一直在运行,这种任务CPU的利用率很高。

cpu密集型任务虽然可以并行完成,但是并行任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以要最高效地利用cpu,cpu密集型任务并行执行的数量等于cpu核数,即线程数等于cpu核数。
示例代码如下:

public class CpuIntenseTargetThreadPoolLazyHolder {
    //线程池: 用于CPU密集型任务
    private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
            MAXIMUM_POOL_SIZE,
            MAXIMUM_POOL_SIZE,
            KEEP_ALIVE_SECONDS,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue(QUEUE_SIZE),
            new CustomThreadFactory("cpu"));


    public static ThreadPoolExecutor getInnerExecutor() {
        return EXECUTOR;
    }

    static {
        log.info("线程池已经初始化");

        EXECUTOR.allowCoreThreadTimeOut(true);
        //JVM关闭时的钩子函数
        Runtime.getRuntime().addShutdownHook(
                new ShutdownHookThread("CPU密集型任务线程池", new Callable() {
                    @Override
                    public Void call() throws Exception {
                        //优雅关闭线程池
                        shutdownThreadPoolGracefully(EXECUTOR);
                        return null;
                    }
                }));
    }
}

 @Test
    public void testCpuIntenseTargetThreadPool() {
        ThreadPoolExecutor pool = CpuIntenseTargetThreadPoolLazyHolder.getInnerExecutor();
        for (int i = 0; i < 2; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    for (int j = 1; j < MAX_TURN; j++) {
                        Print.cfo(getCurThreadName() + ", 轮次:" + j);
                    }
                    Print.cfo(getCurThreadName() + " 运行结束.");
                }
            });

        }
        ThreadUtil.sleepMilliSeconds(Integer.MAX_VALUE);
    }
 

混合型任务

此类任务既要执行逻辑计算,又要进行IO操作。相对来说, 由于执行IO操作的耗时比较长,CPU利用率也不是很高, Web服务器的HTTP请求处理操作属于此类,一次请求处理会包括DB操作、PRC操作、缓存操作等多种耗时操作。一般来说,一次Web请求的CPU计算耗时往往较少,大致在100~500毫秒,而其他耗时操作会占用500到1000毫米。

例如,Tomcat服务器的线程池,一次Web请求的大概由CPU操作+多次数据库操作+多次远程调用组成, CPU计算耗时往往比较少,大致在100ms-500ms之间, 而其他耗时操作会占用500ms-1000ms甚至更多的时间。所以Tomcat服务器的线程池就是典型的混合型任务线程池。

在为混合型任务创建线程池是,可以通过一个比较成熟的估算公式,如下

最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*cpu核数

简化后:

最佳线程数=(线程等待时间/线程cpu时间+1)*cpu核数

举个例子, 比如web服务器处理HTTP请求时,假设平均线程CPU运行为100ms,而线程等待时间为900ms,CPU核数为8核,根据以上公式估算:

(900ms+100ms)/100ms*8=10*8=80

所以需要线程数为80;

从公式得出: 等待时间所占的比例越高,需要的线程数就越多;cpu韩式所占的比例越高,需要的线程就越少。

公式计算出来的是理论值,仅供生产环境中使用参考,还需要结合系统网络环境和硬件情况(cpu、内存、硬盘读写速度)不断尝试,获取一个符合实际的线程数值。

混合型任务创建参考线程池的代码示例:

//懒汉式单例创建线程池:用于混合型任务
public class MixedTargetThreadPoolLazyHolder {
    //首先从环境变量 mixed.thread.amount 中获取预先配置的线程数
    //如果没有对 mixed.thread.amount 做配置,则使用常量 MIXED_MAX 作为线程数
    private static final int max = (null != System.getProperty(MIXED_THREAD_AMOUNT)) ?
            Integer.parseInt(System.getProperty(MIXED_THREAD_AMOUNT)) : MIXED_MAX;
    //线程池: 用于混合型任务
    private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
            max,
            max,
            KEEP_ALIVE_SECONDS,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue(QUEUE_SIZE),
            new CustomThreadFactory("mixed"));


    public static ThreadPoolExecutor getInnerExecutor() {
        return EXECUTOR;
    }

    static {

        log.info("线程池已经初始化");


        EXECUTOR.allowCoreThreadTimeOut(true);
        //JVM关闭时的钩子函数
        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread("混合型任务线程池", new Callable() {
            @Override
            public Void call() throws Exception {
                //优雅关闭线程池
                shutdownThreadPoolGracefully(EXECUTOR);
                return null;
            }
        }));
    }
}

测试代码如下:

@Test
    public void testMixedThreadPool() {
        System.getProperties().setProperty(MIXED_THREAD_AMOUNT, "80");
        // 获取自定义的混合线程池
        ExecutorService pool =
                ThreadUtil.getMixedTargetThreadPool();
        for (int i = 0; i < 1000; i++) {
            try {
                sleepMilliSeconds(10);
                pool.submit(new CreateThreadPoolDemo.TargetTask());

            } catch (RejectedExecutionException e) {
                e.printStackTrace();
            }
        }
        //等待10s
        sleepSeconds(10);
        Print.tco("关闭线程池");
    }

测试结果如下:
【速看】一招教你快速如何快速确定线程池的线程数_第4张图片

如何设置最大线程数你会了没?

你可能感兴趣的:(java,java,jvm,开发语言)