池的概念在java中也是常见,还有连接池、常量池等,池的作用也是类似的,对于对象、资源的重复利用,减小系统开销,提升运行效率。
线程池的主要功能:
1.减少创建和销毁线程的次数,提升运行性能,尤其是在大量异步任务时
2.可以更合理地管理线程,如:线程的运行数量,防止同一时间大量任务运行,导致系统崩溃
demo
先举个demo,看看使用线程池的区别,线程池:
AtomicLong al = new AtomicLong(0l);
Long s1 = System.currentTimeMillis();
ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 100000, 100, TimeUnit.SECONDS, new LinkedBlockingDeque(20000));
for(int i=0;i<20000;i++){
pool.execute(new Runnable() {
@Override
public void run() {
try {
al.incrementAndGet();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.HOURS);
System.out.println("耗时:"+(System.currentTimeMillis()-s1));
System.out.println("AtomicLong="+al.get());
结果:
耗时:224
AtomicLong=20000
非线程池:
AtomicLong al = new AtomicLong(0l);
Long s1 = System.currentTimeMillis();
for(int i=0;i<20000;i++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
al.incrementAndGet();
} catch (Exception e) {
e.printStackTrace();
}
}
});
t.start();
}
while(true){
if(al.get()==20000){
System.out.println("耗时:"+(System.currentTimeMillis()-s1));
System.out.println("AtomicLong="+al.get());
break;
}
Thread.sleep(1);
}
结果:
耗时:2972
AtomicLong=20000
从耗时看2者相差了13倍,差距还是挺大的,可见有大量异步任务时使用线程池能够提升性能。
线程池的主要参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
线程池的主要参数有这6个:
corePoolSize:核心池的大小,核心池的线程不会被回收,没有任务就处于空闲状态。
maximumPoolSize:线程池最大允许的线程数,
keepAliveTime:当线程数超过corePoolSize时,但小于等于maximumPoolSize,会生成‘临时’线程,‘临时’线程执行完任务不会马上被回收,如果在keepAliveTime时间内,还没有新任务的话,才会被回收。
unit:keepAliveTime的单位。
workQueue:当前线程数超过corePoolSize大小时,新任务会处在等待状态,存在workQueue中。
threadFactory:生成新线程的工厂。
handler:当线程数超过workQueue的上限时,新线程会被拒绝,这个参数就是新线程被拒绝的方式。
其中corePoolSize和maximumPoolSize相对难理解,再详细说明:
1、池中线程数小于corePoolSize,新任务都不排队而是直接添加新线程
2、池中线程数大于等于corePoolSize,workQueue未满,首选将新任务加入workQueue而不是添加新线程
3、池中线程数大于等于corePoolSize,workQueue已满,但是线程数小于maximumPoolSize,添加新的线程来处理被添加的任务
4、池中线程数大于大于corePoolSize,workQueue已满,并且线程数大于等于maximumPoolSize,新任务被拒绝,使用handler处理被拒绝的任务
因此maximumPoolSize、corePoolSize不宜设置过大,否则会造成内存、cpu过载的问题,workQueue而尽量可以大一些。
Executors
虽然ThreadPoolExecutor使用很方便,但是建议大家使用jdk已经设定的几种线程池:
Executors.newCachedThreadPool()(无界线程池,可以进行线程自动回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和Executors.newSingleThreadExecutor()(单个后线程),它们满足大部分的场景需求。
1.newSingleThreadExecutor 单线程线程池
看下它的实现方式:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
把corePoolSize设为1,而workQueue大小设为无限大,因此永远只有一个线程在执行任务,新任务都在workQueue中等待。
2.newFixedThreadPool 固定大小线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
和newSingleThreadExecutor 有些类似,只不过从单线程变成可以指定线程数量,workQueue依旧为无限。
3.newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newCachedThreadPool所有新任务都会被立即执行,corePoolSize 设置为0,maximumPoolSize设置为整数最大值,所有线程超过60秒未被使用,就会被销毁。
workQueue
当前线程数超过corePoolSize大小时,新任务会处在等待状态,存在workQueue中。workQueue的实现方式也就是任务的等待策略,分为3种:有限队列、无限队列、直接提交。
谈谈前2种,jdk使用了LinkedBlockingQueue而非ArrayBlockingQueue,有限队列的优势在于对资源和效率更定制化的配置,但是对比无限队列缺点也很明显:
1).增加开发难度。需要考虑到corePoolSize ,maximumPoolSize,workQueue,3个参数大小,既要使任务高效地执行,又要避免任务超量的问题,对于开发和测试的复杂度提高很多。
2).防止业务突刺。在互联网应用业务量暴增也是必须考虑的问题,在使用无限队列时,虽然执行可能慢一点,但是能保证执行到。使用有限队列,会出现任务拒绝的问题。
综上考虑,更建议使用无限队列,尤其是对于多线程不是很熟悉的朋友们。
拒绝策略
所谓拒绝策略之前也提到过了,任务太多,超过maximumPoolSize了怎么办?当然是接不下了,接不下那只有拒绝了。拒绝的时候可以指定拒绝策略,也就是一段处理程序。
决绝策略的父接口是RejectedExecutionHandler,JDK本身在ThreadPoolExecutor里给用户提供了四种拒绝策略,看一下:
1、AbortPolicy
直接抛出一个RejectedExecutionException,这也是JDK默认的拒绝策略
2、CallerRunsPolicy
尝试直接运行被拒绝的任务,如果线程池已经被关闭了,任务就被丢弃。
3、DiscardOldestPolicy
移除最晚的那个没有被处理的任务,然后执行被拒绝的任务。同样,如果线程池已经被关闭了,任务就被丢弃了
4、DiscardPolicy
悄悄地拒绝任务