Java线程池设置多少线程合适



前言

线程池在Java服务中随处可见,但到底设置多少个线程是合适的往往见仁见智。这里,总结下个人看到的观点,结合个人的经验做一些总结。


一、经典方法

来自《Java并发编程实战》

二、要点分析

1. 确定CPU的数量

int N_CPUS = Runtime.getRuntime().availiableProcessors();

这段代码是JDK提供的方案。一般来说在物理机器上是准确的。但是在云原生/虚拟化环境下,笔者就遇到获取结果比实际数量多的情况,导致线程数开的过多。因此,代码可以这么写,实际运行起来还是要再确认下。此外,所谓的可用处理器,实际上是可用的处理器核心,毕竟可能只有1颗处理器,但是有8个核心。

2. 确定任务类型

工程实践中的任务类型,可能是I/O密集型,可能是计算密集型,也有可能是两者的混合也就是混合型任务。虽然良好的设计应该是I/O过程和计算过程分开,但也会遇到遗留系统,因为种种历史原因有许多反常规的将两者合在一个线程中处理的逻辑。因此,凭经验确定的任务类型可能并不准确。最终,通过WAIT/COMPUTE比例来衡量任务类型更为合理。作为开发的我们,还是需要用数据说话。因此让这段逻辑连续运行几次,就能见分晓。以下命令和结果均为CentOS。

查看线程状态
// tpid 为 thread id,此处以grpc boss event loop thread为例
cat /proc/${tpid}/status

cat /proc/27792/status
Name:	grpc-nio-boss-E
Umask:	0022
State:	S (sleeping)
Tgid:	27738
Ngid:	0
Pid:	27792
PPid:	1
TracerPid:	0
Uid:	18930	18930	18930	18930
Gid:	1001	1001	1001	1001
FDSize:	256
Groups:	1001 3007 3030 
VmPeak:	 2958948 kB
VmSize:	 2958944 kB
VmLck:	       0 kB
VmPin:	       0 kB
VmHWM:	  550244 kB
VmRSS:	  550244 kB
RssAnon:	  536192 kB
RssFile:	   14052 kB
RssShmem:	       0 kB
VmData:	 2795616 kB
VmStk:	     132 kB
VmExe:	       4 kB
VmLib:	   19012 kB
VmPTE:	    1460 kB
VmSwap:	       0 kB
Threads:	53
SigQ:	0/14120
SigPnd:	0000000000000000
ShdPnd:	0000000000000000
SigBlk:	0000000000000004
SigIgn:	0000000000000003
SigCgt:	2000000181005ccc
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	0000001fffffffff
CapAmb:	0000000000000000
Seccomp:	0
Speculation_Store_Bypass:	vulnerable
Cpus_allowed:	3
Cpus_allowed_list:	0-1
Mems_allowed:	00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list:	0
# 线程自愿上下文切换
voluntary_ctxt_switches:	12127
# 线程非自愿上下文切换
nonvoluntary_ctxt_switches:	4

非自愿上下文切换 / 资源上下文切换非常大 说明线程经常主动放弃CPU时间片,那通常是等待其他资源,例如I/O完成, 等待锁等。这里的单位是次,能够定性,但不能提供精确的运行/等待比例。

查看线程调度状态
// pid: process id, tpid: thread id
cat /proc/${pid or tpid}/sched


cat /proc/27792/sched
grpc-nio-boss-E (27792, #threads: 53)
-------------------------------------------------------------------
se.exec_start                                :   10308747122.791804
// 虚拟运行总时间(等待时间+使用CPU时间)
se.vruntime                                  :      52693327.134406
// 实际运行总时间(使用CPU时间)
se.sum_exec_runtime                          :          1285.444999
// 跨CPU核心切换次数
se.nr_migrations                             :                 2446
// 上下文切换总次数
nr_switches                                  :                12131
// 自愿切换次数
nr_voluntary_switches                        :                12127
// 非自愿切换次数
nr_involuntary_switches                      :                    4
se.load.weight                               :                 1024
policy                                       :                    0
// 线程优先级
prio                                         :                  120
clock-delta                                  :                   36
mm->numa_scan_seq                            :                    0
numa_migrations, 0
numa_faults_memory, 0, 0, 1, 0, -1
numa_faults_memory, 1, 0, 0, 0, -1

通过se.se.vruntime ,se.sum_exec_runtime,我们可以得到一个相对合理的Wait/Compute比例。

严格来说,这个比例也不是那么地准确。因为,如果线程的任务有诸多的条件分支,导致任务实际是各不相同的,则该比例会有大的波动,失去参考意义。因此,糟糕的设计会带来无数的不确定性,这种不确定性限制了经验和理论的作用。

到这里,我们可以套用公式,HAPPY地去确定线程数了。不过实际情况可能还有些复杂,我们聊聊这些限制。

三、工程限制

纵向限制

线程池中任务依赖其他下游资源。比如连接池中的连接。假设线程池设置为30,而连接池最大仅允许20,那么依然有10线程因获取不到链接而等待。又比如通过网络大批量传输数据,内核TCP窗口太小或对方接收太慢,导致Socket发送缓存总是被填满,此时发送线程也只能等待。

横向限制

JVM可支配内存不足以支持更多的并发;通常进程中线程池不止一个,存在多个同类任务的线程池,线程优先级设置等都会影响最终的W/C比例。

总结

以上就是今天要讲的内容,本文对设置线程数的方法,要素和工程限制结合个人做了些总结,希望对你有所帮助。如您有更好的方法,也欢迎拍砖,感谢您的阅读。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中,线程池和阻塞队列(BlockingQueue)的选择与设置是非常关键的,因为它们直接影响了系统的并发性能和资源管理。线程池中的阻塞队列用于存储任务,当线程池中的工作线程空闲时,会从队列中取出任务执行;反之,如果任务队列满了,新提交的任务会被阻塞,直到队列中有空间。 决定队列大小的因素通常包括: 1. **系统负载**:考虑应用程序的平均请求速率以及峰值负载。如果预计请求量波动较大,可能需要较大的缓冲能力来应对高峰期。 2. **硬件限制**:比如内存大小。队列过大会消耗过多内存,影响其他部分的性能。队列过小可能导致频繁地创建和销毁线程,增加上下文切换成本。 3. **任务类型**:对于I/O密集型任务,队列可以适当大一些,因为它不需要立即处理结果;而对于计算密集型任务,队列可能需要更小,以免CPU等待IO完成。 4. **超时策略**:如果你希望在队列满时设定一个时间限制(如生产者阻塞的时间),那么队列大小应该足够容纳这个超时时间内的任务。 常用的Java阻塞队列有`ArrayBlockingQueue`、`LinkedBlockingQueue`、`PriorityBlockingQueue`等,它们的配置参`容量`就是指队列的最大元素量。一般来说,初始容量可以设置为核心线程的1.5到3倍,然后根据实际情况调整。同时,也可以设置无界队列(如`LinkedList`或自定义无限大小的队列),但这样可能会导致大量内存占用。 设置合适的队列大小需要进行实际监控和测试,找到一个既满足吞吐需求又能保证系统稳定性的平衡点。如果你提供具体的应用场景或据,我可以给出更具体的建议。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值