java线程池最大线程数-java线程池原理
每篇文章一句话
烧不死的鸟就是火凤凰,烧死了就会变成烤鸽子。
1 概述
在Java中,我们通常通过集成Thread类并实现Runnnable接口,调用线程的start()方法来启动线程。 但是如果并发量很大,每个线程执行很短时间就结束了,这种频繁的线程创建和进程销毁会大大降低系统运行的效率。 线程池的产生是为了解决多线程效率低下的问题。 它允许线程被重用,即线程执行后不被销毁,而是可以继续执行其他任务。 (这里可以以tomcat为例来思考)
很多人要问,线程池听上去高大上,实际工作中却很少用到。 事实上,池化技术在各种流行的框架或高性能架构中无处不在。 那么有人要问了java线程池最大线程数,线程池有什么用呢?
一句话,就是提高系统效率和吞吐量。 如果服务器为每个请求创建一个线程,那么短时间内会发生很多创建和销毁动作。 然而,服务器在创建和销毁线程上花费大量时间和消耗系统资源。 线程池可以尽量减少这种情况的发生。
所以,java.util.concurrent.ThreadPoolExecutor类(java5之后出现,由大师Doug Lea完成),不得不说,它是今天的主菜
2.栗子
一、ThreadPoolExecutor的重要参数
corePoolSize:核心线程数
queueCapacity:任务队列容量(阻塞队列)
maxPoolSize:最大线程数
keepAliveTime:线程空闲时间
当线程空闲时间达到keepAliveTime时,线程将被销毁,直到线程数=corePoolSize
如果allowCoreThreadTimeout=true,直到线程数=0(这个特性需要注意)
allowCoreThreadTimeout:允许核心线程超时(如上,会影响keepAliveTime)
rejectedExecutionHandler:任务拒绝处理程序(用户可以自定义拒绝后的处理方式)
两种情况会拒绝处理任务:
1.当线程数达到maxPoolSize且任务队列已满时,新任务将被拒绝
2、当线程池被调用shutdown()时,会等待线程池中的任务执行完毕后才关闭。 如果在调用 shutdown() 和线程池实际关闭之间提交了任务,则新任务将被拒绝(它不会立即停止,而是在执行后停止)。
如果被拒绝,此时线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认值为AbortPolicy,会抛出异常
hreadPoolExecutor 类有几个内部实现类来处理这种情况:
1:AbortPolicy丢弃任务并抛出运行时异常
2:CallerRunsPolicy执行任务(该策略会重试添加当前任务,会自动重复调用execute()方法,直到成功)如果执行器关闭,则丢弃。
3:DiscardPolicy直接静默丢弃被拒绝的任务,无异常信息
4:DiscardOldestPolicy不丢弃被拒绝的任务,而是丢弃队列中等待时间最长的线程(队头的任务会被删除),然后将被拒绝的任务加入队列(Queue是first-in- first-out任务调度算法,具体策略下面会分析)(如果再次失败,重复该过程)
5:实现RejectedExecutionHandler接口,可以自定义处理器(可以自己实现然后设置进去)
2、ThreadPoolExecutor处理任务的顺序和原理
通过 execute(Runnable) 方法将任务添加到线程池中。 任务是一个Runnable类型的对象,任务的执行方法是Runnable类型对象的run()方法。
当通过execute(Runnable)方法向线程池添加任务时,线程池采用的策略如下(即添加任务的策略):
如果此时线程池中的数量小于corePoolSize,即使线程池中的线程空闲,也会创建新的线程来处理新增的任务。
如果此时线程池中的个数等于corePoolSize,但是缓冲队列workQueue没有满,则将任务放入缓冲队列。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue已满,线程池中的数量小于maximumPoolSizejava线程池最大线程数,则创建新的线程来处理新增的任务。
如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue已满,线程池中的数量等于maximumPoolSize,则通过handler指定的策略处理任务。
任务处理的优先级(顺序)为:
如果核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize都满了,使用handler来处理被拒绝的任务。 当线程池中的线程数大于corePoolSize时,如果一个线程空闲时间超过keepAliveTime,该线程就会被终止。 这样,线程池就可以动态调整池中线程的数量。
简要总结如下:
当线程数小于核心线程数时创建线程。
当线程数大于等于核心线程数且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列满时
如果线程数小于最大线程数,则创建一个线程
如果线程数等于最大线程数,则抛出异常并拒绝任务
线程池处理流程图:
这里提醒一下:如果失败处理策略选择了DiscardOldestPolicy,可能会丢失任务。
另外,Executors类中还有几个方法:newFixedThreadPool()、newCachedThreadPool()等方法,实际上是间接调用了ThreadPoolExocutor,只是传递了不同的构造参数。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
Executors.newCachedThreadPool(); //创建一个缓冲池,容量为Integer.MAX_VALUE
Executors.newSingleThreadExecutor(); //创建一个容量为1的缓冲池
Executors.newFixedThreadPool(int); //创建一个固定容量的缓冲池
ThreadPoolExecutor的继承关系如下
执行器->ExecutorService->AbstractExecutorService->ThreadPoolExecutor
其中有一个构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
发现我们可以指定workQueue和handler。当然还有其余的构造函数,有类似的效果
线程池构造函数的7个参数解释:
强烈建议程序员使用更方便的Executors工厂方法Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和Executors.newSingleThreadExecutor() (单后台线程),它具有适用于大多数使用场景的预定义设置。
但是,但是,但是。 . . 使用 fix 有一些陷阱。 具体可以看我的博文(生产环境中的一个活生生的杀人案):
另外,这里我说一下ThreadPoolTaskExecutor:
ThreadPoolTaskExecutor是spring的线程池技术。 其实它的实现完全是使用ThreadPoolExecutor实现的(有点类似于装饰器模式,当然Spring提供的功能更强大,因为还有定时调度功能)。
3、如何设置线程池的参数:
系统默认
核心池大小=1
queueCapacity=Integer.MAX_VALUE
maxPoolSize=Integer.MAX_VALUE
keepAliveTime=60s
allowCoreThreadTimeout=false
rejectedExecutionHandler = AbortPolicy()
那我们怎么设置呢?需要根据几个值来决定
tasks : 每秒任务数,假设500~1000
taskcost:每个任务花费的时间,假设0.1s
responsetime:系统允许的最大响应时间,假设为1s
做一些计算
corePoolSize = 每秒需要多少个线程?
threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100个线程。 corePoolSize 设置应大于 50
根据8020原则,如果每秒80%的任务小于800,则设置corePoolSize为80
queueCapacity = (coreSizePool/taskcost)*响应时间
计算可以是queueCapacity = 80/1 = 80。表示队列中的线程可以等待1s,超过则需要开新线程执行
记住不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小。 当任务激增时,无法开启新的线程执行,响应时间会激增。
maxPoolSize = (max(tasks)-queueCapacity)/(1/taskcost)
计算 maxPoolSize = (1000-80)/10 = 92
(最大任务数-队列容量)/每线程每秒处理能力=最大线程数
rejectedExecutionHandler:视具体情况而定。 如果任务不重要,可以放弃。 如果任务很重要,应该使用一些缓冲机制来处理它。
keepAliveTime 和 allowCoreThreadTimeout 通常默认满足
以上为理想值,实际情况需根据机器性能而定。 如果在未达到最大线程数时机器cpu负载已经满了,就需要升级硬件(呵呵)和优化代码来降低taskcost。
JDK1.5的线程池由Executor框架提供。 Executor 框架将处理请求任务的提交与其执行分离。 可以制定执行策略。 在线程池中执行线程可以复用现有线程而不是创建新线程,这样可以抵消处理多个请求时线程创建和消亡的开销。 如果线程池太大,会导致内存占用过高,也会耗尽资源。 如果它太小,则会由于存在许多不工作的处理器资源而导致吞吐量损失。
如何合理配置线程池大小,一般需要根据任务类型配置线程池大小:
1、如果是CPU密集型任务,需要尽可能的压榨CPU。 参考值可以设置为NCPU+1(比如4核就配置为5)
2.如果是IO密集型任务,参考值可以设置为2*NCPU
当然,这只是一个参考值,具体设置还需要根据实际情况进行调整。 例如,可以先设置线程池大小作为参考值,然后观察任务运行状态、系统负载、资源利用率等情况,做出适当的调整。
三、使用场景
1.当你的任务不是必须的,比如记录操作日志,通知第三方服务不需要的信息等,可以使用线程池来处理非阻塞任务
2.当你的任务很耗时的时候,可以使用线程池技术
3.当请求并发较高时,可以使用线程池技术优化处理
线程池可以通过 Executors 静态工厂来构建,但一般不推荐这样做。
提醒:当线程池可以使用的时候,不要自己去开启新的线程。 在高并发环境下,系统资源是宝贵的,需要节约资源来提高可用性。
复活节彩蛋
Executors 工具类为我们提供了许多快速创建线程池的方法,尽管我们不推荐使用它们。 但是里面有一个方法我觉得还不错:Executors.newSingleThreadExecutor() 如果用这个作为全局线程池,可以很好的实现异步,同时也可以保证任务的顺序执行,从而实现消峰效果:
private static final ExecutorService executorService = Executors.newSingleThreadExecutor();
private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
executorService.execute(() -> {
num.getAndIncrement();
System.out.println(Thread.currentThread().getName() + "-->>>>" + num);
});
}
executorService.shutdown();
}
输出:
pool-1-thread-1-->>>>1
pool-1-thread-1-->>>>2
pool-1-thread-1-->>>>3
pool-1-thread-1-->>>>4
pool-1-thread-1-->>>>5
pool-1-thread-1-->>>>6
pool-1-thread-1-->>>>7
pool-1-thread-1-->>>>8
pool-1-thread-1-->>>>9
pool-1-thread-1-->>>>10
pool-1-thread-1-->>>>11
pool-1-thread-1-->>>>12
pool-1-thread-1-->>>>13
pool-1-thread-1-->>>>14
pool-1-thread-1-->>>>15
pool-1-thread-1-->>>>16
pool-1-thread-1-->>>>17
pool-1-thread-1-->>>>18
pool-1-thread-1-->>>>19
pool-1-thread-1-->>>>20
我们发现只有一个pool,也只有一个线程,任务是顺序执行的。 我们在使用Fiexed的时候,也可以达到顺序执行的效果。 因为内部阻塞队列是FIFO的一种实现:
private static final ExecutorService executorService = Executors.newFixedThreadPool(5);
private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "-->>>>" + num.getAndIncrement());
});
}
executorService.shutdown();
}
输出:
pool-1-thread-2-->>>>0
pool-1-thread-1-->>>>1
pool-1-thread-2-->>>>2
pool-1-thread-1-->>>>3
pool-1-thread-2-->>>>4
pool-1-thread-1-->>>>5
pool-1-thread-2-->>>>6
pool-1-thread-1-->>>>7
pool-1-thread-2-->>>>8
pool-1-thread-1-->>>>9
pool-1-thread-2-->>>>10
pool-1-thread-1-->>>>11
pool-1-thread-2-->>>>12
pool-1-thread-1-->>>>13
pool-1-thread-2-->>>>14
pool-1-thread-1-->>>>15
pool-1-thread-2-->>>>16
pool-1-thread-4-->>>>17
pool-1-thread-3-->>>>18
pool-1-thread-5-->>>>19