java线程数量如何确定

1. 概述

  • 使用线程池的好处

    • 降低资源消耗: 线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,通过重复利用己创建的线程可以降低线程创建和销毁造成的消耗。
    • 提高响应速度: 当任务到达时,可以不需要等待线程创建就能立即执行
    • 提高线程的可管理性: 线程池提供了一种限制、管理资源的策略,维护一些基本的线程统计信息,如已完成任务的数量等。通过线程池可以对线程资源进行统一的分配、监控和调优。

    虽然使用线程池的好处很多,但是如果其线程数配置得不合理,不仅可能达不到预期效果,反而可能降低成用的性能

2. 线程池分类

2.1 按照任务类型对线程池进行分类

使用标准构造器ThreadPoolExecutor创建线程池时,会涉及线程数的配置,而线程数的配置异步任务类型是分不开的。

  1. IO密集型任务: 此类任务主要是执行IO操作。由于执行IO操作的时间较长,导致CPU的利用率不高,这类任务CPU常处于空闲状态。Netty的IO读写操作为此类任务的典型例子。
  2. CPU密集型任务: 此类任务主要是执行计算任务。由于响应时间很快,CPU一直在运行,这种任务CPU的利用率很高。
  3. 混合型任务: 此类任务既要执行逻辑计算,又要进行IO操作(如RPC调用、数据库访问)。相对来说,由于执行IO操作的耗时较长(一次网络往返往往在数百毫秒级别),这类任务的CPU利用率也不是太高。Web服务器的HTTP请求处理操作为此类任务的典型例子。

一般情况下,针对以上不同类型的异步任务需要创建不同类型的线程池,并进行针对性的参数配置。

2.2 为 IO密集型任务确定线程数

  • 由于IO密集型任务的CPU使用率较低,导致线程空余时间很多,因此通常需要开CPU核心数两倍的线程。
  • IO线程空闲时,可以启用其他线程继续使用CPU,以提高CPU的使用率。Netty的IO处理任务就是典型的IO密集型任务。所以,Netty的Reactor(反应器)实现类(定制版的线程池)的IO处理线程数默认正好为CPU核数的两倍
//多线程版本Reactor反应器组
public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup{
    //IO事件处理线程数
	private static final int DEFAULT_EVENT_LOOP_THREADS;
    static {
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1,
                SystemPropertyUtil.getInt("io.netty.eventLoopThreads",
                Runtime.getRuntime().availableProcessors() * 2));                   
    }
    protected MultithreadEventLoopGroup(int nThreads,
                                        ThreadFactory threadFactory,
                                        Object...args){
        super(nThreads =0?
              DEFAULT_EVENT_LOOP_THREADS : nThreads,threadFactory,args);
    }
}

IO密集型demo代码

public class ThreadUtil {
    
    /**
     * CPU核数
     **/
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int IO_MAX = Math.max(2, CPU_COUNT * 2);
    private static final int KEEP_ALIVE_SECONDS = 30;
    private static final int QUEUE_SIZE = 128;
    private static class IoIntenseTargetThreadPoolLazyHolder {
        //线程池: 用于IO密集型任务
        private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
            IO_MAX,
            IO_MAX,
            KEEP_ALIVE_SECONDS,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue(QUEUE_SIZE),
            new CustomThreadFactory("io"));

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

  • IO线程池调用了allowCoreThreadTimeOut(..)方法,并且传入了参数truekeepAliveTime参数所设置的Idle超时策略也将被应用于核心线程,当池中的线程长时间空闲时,可以自行销毁。
  • 使用有界队列缓冲任务而不是无界队列,如果128太小,可以根据具体需要进行增大,但是不能使用无界队列
  • corePoolSizemaximumPoolSize不一致时,当corePoolSize满了而maximumPoolSize没满时即使可以创建线程,但是此时线程池默认不会创建线程,而是将任务加入阻塞队列,等待核心线程空闲,而如果核心线程不空闲,那么任务得不到执行。
  • 如果corePoolSize和maximumPoolSize保持一致,使得在接收到新任务时,如果没有空闲工作线程,就优先创建新的线程去执行新任务,而不是将任务优先加入阻塞队列且等待现有工作线程空闲后再执行。
  • 使用懒汉式单例模式创建线程池,如果代码没有用到此线程池,就不会立即创建。
  • 使用JVM关闭时的钩子函数优雅地自动关闭线程池。

2.3 为 CPU 密集型任务确定线程数

  • CPU密集型任务也叫计算密集型任务,其特点是要进行大量计算而需要消耗CPU资源,比如计算圆周率、对视频进行高清解码等。
  • CPU密集型任务虽然也可以并行完成,但是并行的任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以要最高效地利用CPU,CPU密集型任务并行执行的数量应当等于CPU的核心数。比如说4个核心的CPU,通过4个线程并行执行4个CPU密集型任务,此时的效率是最高的。但是如果线程数远远超出CPU核心数量,就需要频繁地切换线程,线程上下文切换时需要消耗时间,反而会使得任务效率下降。因此,对于CPU密集型的任务来说,线程数等于CPU数就行。
public class ThreadUtil {
    
    /**
     * CPU核数
     **/
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int KEEP_ALIVE_SECONDS = 30;
    private static final int QUEUE_SIZE = 128;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT;

    private static 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"));

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

2.4 为混合型任务确定线程数

  • 混合型任务既要执行逻辑计算,又要进行大量非CPU耗时操作(如RPC调用、数据库访问、网络通信等),所以混合型任务CPU利用率不是太高,非CPU类型耗时往往是CPU耗时的数倍。比如在Web应用处理HTTP请求处理时,一次请求处理会包括DB操作、RPC操作、缓存操作等多种耗时操作。一般来说,一次Web请求的CPU计算耗时往往较少,大致在100500毫秒,而其他耗时操作会占用5001000毫秒,甚至更多的时间。

  • 在为混合型任务创建线程池时,如何确定线程数呢?业界有一个比较成熟的估算公式,具体如下:

    最佳线程数 = (( 线程等待时间 + 线程CPU时间 ) / 线程CPU时间 ) * CPU核数
    
    经过简单的换算,以上公式可进一步转换为:
    
    最佳线程数目 = ( 线程等待时间 与 线程CPU时间 之比 + 1) * CPU核数
    

    通过公式可以看出:

    1. 等待时间所占比例越高,需要的线程就越多
    2. CPU耗时所占比例越高,需要的线程就越少。

    例子:

    • 比如在Web服务器处理HTTP请求时,假设平均线程CPU运行时间为100毫秒,而线程等待时间为900毫秒,如果CPU核数为8,那么根据上面这个公式,估算如下:

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

      经过计算,以上案例中需要的线程数为80。

    线程数越高越好吗?使用很多线程是否就一定比单线程高效呢?答案是否定的,比如大名鼎鼎的Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/秒。

  • 创建混合型线程池时,建议按照前面的最佳线程数估算公式提前预估好线程数(如80),然后设置在环境变量mixed.thread.amount中,测试用例如下:

public class ThreadUtil {
    private static final int MIXED_CORE = 0;  //混合线程池核心线程数
    private static final int MIXED_MAX = 128;  //最大线程数
    public static final String MIXED_THREAD_AMOUNT = "mixed.thread.amount";
    
    //懒汉式单例创建线程池:用于混合型任务
    private static 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"));
        
        static {
            EXECUTOR.allowCoreThreadTimeOut(true);
            //JVM关闭时的钩子函数
            Runtime.getRuntime().addShutdownHook(new ShutdownHookThread("混合型任务线程池", new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    //优雅关闭线程池
                    shutdownThreadPoolGracefully(EXECUTOR);
                    return null;
                }
            }));
        }
    }
}

posted on   ccblblog  阅读(125)  评论(0编辑  收藏  举报

(评论功能已被禁用)
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示