线程池中线程数的多少一直是让人很迷惑的一个点,把控不住到底设置多少个合适,查相关资料的话,网上这个话题实际有用的内容都很少。

其实可以分两种情况,第一种情况是业务需要严格把控线程使用率,这种其实多数遇不到,只有在极端严格的开发标准中才会用到,因为在这种情况下,线程池中的线程数往往是个位数,这也是为什么JAVA里面有一个单线程池的原因。大家日常在使用线程池的时候,往往对单线程池不太关注,多数用的是定长线程池,但其实单线程池往往是高标准开发的最优解。不过我们用多线程就是为了提高资源使用率以及任务的执行效率,因此这种情况和我们一般情况的需求有一些不成正比。

在高标准情况下,无非就是两个目的,计算处理或IO任务,大家可以参考如下的设置方式:

如果线程池是用来计算的,则程池的大小应为 N+1 ,N为处理器个数。
如果线程池是用来处理I/O的,则线程池大小为N*(1+W/C) ,W是I/O 资源获取时间,C是处理时间。

当然大多数还属于一般情况,这就有些复杂了,因为它涉及到一个oom的问题,当然不是说高标准高要求情况下就不会爆发oom,相反,也会发生,只是发生时反应出的问题很直白,就是单纯的资源不够。可是一般情况下,我们会设置很多线程数,所以就要考虑是不是线程数设置多了?而设置少了又达不到高效率的目标。

很多人都知道oom这个报错,但很少有人真触发过它,甚至都不知道它的全称是什么?所以需要先给大家解释一下oom到底是什么东西?oom的报错全称是java.lang.OutOfMemoryError: unable to create new native thread。它是指当一个Java进程所需要的实际内存,超过了它所能占用的最大内存时,爆的一个错误。

有不少新手可能以为oom是由于JAVA虚拟机的内存不够了,其实并不是这个样子,反而如果JAVA虚拟机所占内存过大,就更容易爆发oom。

这个问题的核心点在于,我们使用线程池无论设置多少个线程数,它都是在主进程下的若干个子线程,所共享的都是主进程的内存资源,而在不同的系统上,能够允许一个进程所占用的最大内存数是不一样的,在Windows体系中,32位的系统只允许一个进程最大占4g内存资源,而且其中2g还要给系统,也就是说一个进程能够实际使用到的最大内存也就2g,linux系统中是3g,而且这个值无法往大调,这是32位系统无法摆脱的限制,大家可以网上搜一篇文献High Memory in the Linux Kernel,里面有讲到这个问题。

这也是为什么我们现在服务器跑Linux系统的时候都是64位,因为在64位的体系下,无论是WINDOWS还是linux,单线程所能最大占用内存的这个限制,虽然仍然存在,但是这个限制值就被放的很大,大到我们可以忽视掉这条限制,再配合上Java1.8以后可以通过配置来设置一个虚拟机最小和最大占用内存。甚至在标准的Linux内核中,对于内存资源的把控是1比3比例,也就是说,内存的1/4是系统使用的,3/4会提供给进程使用。因此在64位系统上,多数情况下我们可以给一个程序很大的占用上限,大到不爆oom,而是系统直接把进程kill掉了。

这里要特别说明一下,有很多人的思路停留在oom爆发于Java虚拟机内存不够的情况下,可能一时反应不过来,上面说的。要知道一个Java进程在启动后,虚拟机所占内存不是一直不变,当不够的时候,它会自动扩大,而当它扩大到系统所能允许的最大内存时,已经没有办法扩大了oom就会爆出来。而程序运行的时候,程序本身所用内存以及程序执行时所用内存都算在了堆内存里,所以通常情况下都是由于堆内无法继续放大,其他虚拟机区域一般是存的引用所占的很小的。

知道了oom是什么东西,我们在来说一般情况下需要较多的线程时设置多少的问题。其实还是需要大家自己量力而行,根据实际情况而定,但是有一个可以参考的理论巅峰值公式。

(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = 理论最大线程数

在公式中MaxProcessMemory指的是一个进程能占用的最大系统内存,JVMMemory是JVM 内存,ReservedOsMemory是操作系统保留内存,ThreadStackSize是Java线程栈的大小。

在一个Java进程创建并启动以后,留给进程本身能够使用的内存数就只有了MaxProcessMemory - JVMMemory - ReservedOsMemory的计算结果大小了,这就意味着我上面说过的一点,jvm所用内存越大,java进程运行时可用的内存也就越小,oom爆发的可能也就越高。对于系统最大内存上面也说了,如果你用的是32位的系统将很受限制,所以你可以用64位的,jvm内存和线程栈大小都可以通过jdk的配置去改。

如果真的有优化的需求,请注意你不要用开发工具边跑任务边优化,因为开发工具是自带jdk的,不要最后你改了半天本地的jdk最后没用到,就尴尬了。

网上有人算过在32位的windows下,系统保留内存按130M算,开发工具用eclipse默认启动的JVMMemory是64M,JDK 1.6默认的线程栈是325K,套一下公式就是(2*1024*1024-64*1024-130*1024)/325 = 5841

当然这个数值参与的计算资料就太老了,就拿我们现在所用的大部分都是64位系统来说,就可以把结果推到一个很高的程度。所以随着业内各体系的不断完善,无论是操作系统还是服务器本身的性能,都会让我们慢慢的把能关注设置多少个这个问题,转变成去关注设置多少个合适?就比如说如果你的线程池操作的目的端也是一个池,比方说是一个数据库的连接池,那你就要考虑设置多少合适,而不是能设置多少,两方之间会形成一个相互牵制的关系,任意的一方都不能太多或者太少,两者要处于一个平衡的状态。

最后,对于上面的公式要补充一点,它的目的其实就是计算程序刚启动后,在开始正式跑程序前,所剩的可占用内存,最多能够分配给多少个子线程,每个线程都有自己的线程栈,所以除以它的大小,就能算出理论极限下能分出多少个子线程,如果你能够大致估算出每个子线程大概需要占用多少内存,也可以去直接除以估算的结果,这样会更准确。