池的概念在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());
结果:
耗时:224AtomicLong=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); }
结果:
耗时:2972AtomicLong=20000
从耗时看2者相差了13倍,差距还是挺大的,可见有大量异步任务时使用线程池能够提升性能。
线程池的主要参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueworkQueue, 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())); }
把corePoolSize设为1,而workQueue大小设为无限大,因此永远只有一个线程在执行任务,新任务都在workQueue中等待。
2.newFixedThreadPool 固定大小线程池
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }
和newSingleThreadExecutor 有些类似,只不过从单线程变成可以指定线程数量,workQueue依旧为无限。
3.newCachedThreadPoolpublic static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }
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
悄悄地拒绝任务