关于线程池
首先第一点,线程池是需要关闭的!!!
- try {
- // 向学生传达“问题解答完毕后请举手示意!”
- pool.shutdown();
- // 向学生传达“XX分之内解答不完的问题全部带回去作为课后作业!”后老师等待学生答题
- // (所有的任务都结束的时候,返回TRUE)
- if(!pool.awaitTermination(awaitTime, TimeUnit.MILLISECONDS)){
- // 超时的时候向线程池中所有的线程发出中断(interrupted)。
- pool.shutdownNow();
- }
- } catch (InterruptedException e) {
- // awaitTermination方法被中断的时候也中止线程池中全部的线程的执行。
- System.out.println("awaitTermination interrupted: " + e);
- pool.shutdownNow();
- }
shutdown也是很重要的
看了上面的描述后可能有些人会认为,只需要执行awaitTermination和shutdownNow就可以正常结束线程池中的线程了。其实不然。
shutdown方法还有「大家只解答我要求的问题,其它的不用多做」的意思在里面。
shutdown方法调用后,就不能再继续使用ExecutorService来追加新的任务了,如果继续调用execute方法执行新的任务的话
就会抛出RejectedExecutionException异常。(submit方法也会抛出上述异常)
而且,awaitTermination方法也不是在它被调用的时间点上简单得等待任务结束而是在awaitTermination方法调用后,
持续监视各个任务的状态以或者是否线程已经运行结束。所以不调用shutdown方法执行调用awaitTermination的话由于追加出来的任务可能
会导致任务状态监视出现偏差而发生预料之外的awaitTermination的Timeout异常
正确的调用顺序是
shutdown方法
awaitTermination方法
shutdownNow方法(发生异常或者是Timeout的时候)
实际开发的系统可能会有不能强制线程中止执行的场景出现,所以虽然推荐使用上面说的调用顺序但也并不是绝对一成不变的。
另外,可以经过一定时间间隔而有计划调用任务执行的ScheduledExecutorService同样适用于上面说的调用顺序,但是在使用scheduled方法的时候需要另外一些步骤。
2:如何创建一个线程池:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
这里只是创建线程池其中的一个构造函数;其实其他的构造函数最终还是调用的这个构造函数;
说明一下这些参数的作用:
corePoolSize:核心池的大小,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;这个参数是跟后面的阻塞队列联系紧密的;只有当阻塞队列满了,如果还有任务添加到线程池的话,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务;如果继续添加任务到线程池,且线程池中的线程数已经达到了maximumPoolSize,那么线程就会就会执行reject操作(这里后面会提及到)
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止;默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用;即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法并设置了参数为true,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的阻塞队列大小为0;(这部分通过查看ThreadPoolExecutor的源码分析--getTask()部分);
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性(时间单位)
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
threadFactory:线程工厂,主要用来创建线程:默认值 DefaultThreadFactory;
handler:表示当拒绝处理任务时的策略,就是上面提及的reject操作;有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。(默认handle)
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
3:对线程池的基本使用及其部分源码的分析(注意:这里的源码分析是基于jdk1.6;)
a:线程池的状态
volatile int runState;
static final int RUNNING = 0; 运行状态
static final int SHUTDOWN = 1; 关闭状态;SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕
static final int STOP = 2;停止状态;此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务
static final int TERMINATED = 3;终止状态;当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态
b:参数再次说明。这是摘自网络的解释,我觉得他比喻的很好,所以这里直接就用它的解释
这里要重点解释一下corePoolSize、maximumPoolSize、largestPoolSize三个变量。
corePoolSize在很多地方被翻译成核心池大小,其实我的理解这个就是线程池的大小。举个简单的例子:
假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。
因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;
当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;
如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;
然后就将任务也分配给这4个临时工人做;
如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。
当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。
不过为了方便理解,在本文后面还是将corePoolSize翻译成核心池大小。
largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系。
---------------------
作者:Believe勤能补拙
来源:CSDN
原文:https://blog.csdn.net/a355586533/article/details/78427871
版权声明:本文为博主原创文章,转载请附上博文链接!
4.总结:
ThreadPoolExecutor的三种队列SynchronousQueue,LinkedBlockingQueue,ArrayBlockingQueue
SynchronousQueue
SynchronousQueue是无界的,是一种无缓冲的等待队列,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加;可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。
声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
LinkedBlockingQueue
LinkedBlockingQueue是无界的,是一个无界缓存的等待队列。
基于链表的阻塞队列,内部维持着一个数据缓冲队列(该队列由链表构成)。当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。
LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
ArrayListBlockingQueue
ArrayListBlockingQueue是有界的,是一个有界缓存的等待队列。
基于数组的阻塞队列,同LinkedBlockingQueue类似,内部维持着一个定长数据缓冲队列(该队列由数组构成)。ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。
ArrayBlockingQueue和LinkedBlockingQueue是两个最普通、最常用的阻塞队列,一般情况下,处理多线程间的生产者消费者问题,使用这两个类足以。