写在前面:各位看到此博客的小伙伴,如有不对的地方请及时通过私信我或者评论此博客的方式指出,以免误人子弟。多谢!
前面了解了Fork/Join线程池,再记录一下ThreadPoolExecutor线程池的使用,在使用前先说一下在《阿里巴巴java开发手册》中对并发处理的要求:1.线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程。2.线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这样的处理方式可以更加明确线程池的运行规则,避免资源耗尽的风险。
那为什么要使用线程池呢?
1.通过重用已存在的线程,减少在创建和销毁线程上所消耗的时间以及系统资源的开销。
2.提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
3.方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。
那为什么不允许使用Executors创建线程池呢?
因为使用Executors去创建线程池是有风险的,如下:
1.FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
2.CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
PS:之前文章中使用Executors创建的线程可以改为ThreadPoolExecutor方式。
那线程池数量为多少合适呢?
线程池数量:
网上看到一个公式:Nthreads=Ncpu*Ucpu*(1+W/C)
Nthreads=CPU数量,Ucpu=目标CPU的使用率,0<=Ucpu<=1,W/C=任务等待时间与任务计算时间的比率。
说完这些,接下来只记录下ThreadPoolExecutor 线程池,先看下创建ThreadPoolExecutor 线程池的源码:
参数介绍:
1.int corePoolSize:核心线程数
默认为0,当有任务来时就创建一个线程,当任务数量超过corePoolSize时会将任务放入任务队列,当任务队列也超了会继续创建新线程,直到线程数为maximumPoolSize。
2.int maximumPoolSize:线程池最大线程数
当线程数>=corePoolSize,且任务队列已满时。如果还有任务添加到线程池,线程池会创建新线程来处理任务。
当任务队列已满时,如果还有任务添加到线程池,线程池会执行reject操作。
3.long keepAliveTime:线程空闲时间
默认情况,当线程数大于corePoolSize时,keepAliveTime才会生效,将空闲时长超过设定值的线程终止,直到线程数量等于corePoolSize,但是如果调用了allowCoreThreadTimeOut(boolean)方法并设置了参数为true,在线程池中的线程数不大于corePoolSize时, keepAliveTime参数也会起作用。
4.TimeUnit unit:参数keepAliveTime的时间单位.
5.BlockingQueue<Runnable> workQueue:任务队列
它分为直接提交队列、有界任务队列、无界任务队列、优先任务队列;
1)SynchronousQueue:直接提交队列,它没有容量,不存储任务的阻塞队列,每一个存入对应一个取出,串行化队列,当任务超出maximumPoolSize,线程池会执行reject操作.
2)ArrayBlockingQueue:有界的任务队列,按FIFO的规则对任务进行存取,使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。
3)无界的任务队列:LinkedBlockingQueue,按FIFO(先进先出)的规则存取任务,最大容量Integer.MAX_VALUE, 可能会将资源耗尽,使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。
4)PriorityBlockingQueue:有优先级的任务队列, PriorityBlockingQueue它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。
6.ThreadFactory threadFactory:线程工厂
主要用来创建线程:默认值 DefaultThreadFactory。
7.RejectedExecutionHandler handler:表示当拒绝处理任务时的策略
1)ThreadPoolExecutor.AbortPolicy:该策略会直接抛出异常(RejectedExecutionException异常),阻止系统正常工作。(默认handle)
2)ThreadPoolExecutor.DiscardPolicy:该策略会默默丢弃无法处理的任务,不予任何处理,但是不抛出异常。
3)ThreadPoolExecutor.DiscardOldestPolicy:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交。
4)ThreadPoolExecutor.CallerRunsPolicy:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行。
5)可自定义拒绝策略。
代码示例:
private static ExecutorService pool = new ThreadPoolExecutor(3,5,1000, TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
for (int i=0;i<10;i++){
pool.execute(new Task1(i));
}
}
static class Task implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果如下:
可以看到只有5个线程执行了,超出最大线程数后,执行了默认的拒绝策略。
将new SynchronousQueue<Runnable>()更换为new ArrayBlockingQueue<>(2)后,预期效果是:仍然5个线程执行,但是会执行7次,并执行拒绝策略报错,因为ArrayBlockingQueue队列中可以缓存2个任务,如果ArrayBlockingQueue初始化的队列长度大于等于5,所有任务会正常执行不再报错,ArrayBlockingQueue初始长度为2时执行结果如下:
ArrayBlockingQueue初始长度为大于等于5时执行结果如下:
更换为LinkedBlockingQueue后,预期效果是有3个线程执行,不会报错,因为多余的任务都放到LinkedBlockingQueue队列中,不再创建线程,执行效果如下:
更换为PriorityBlockingQueue有优先级的任务队列后,并不能保证优先级高的就一定先执行,只是相对来说优先级高的先执行的概率会大一些。
拓展:
1.线程池的关闭
调用线程池的shutdown()或shutdownNow()方法来关闭线程池。
shutdown:按过去执行已提交任务的顺序发起一个有序的关闭,但是不接受新任务。如果已经关闭,则调用没有其他作用。
shutdownNow:尝试停止所有的活动执行任务、暂停等待任务的处理,并返回等待执行的任务列表。在从此方法返回的任务队列中排空(移除)这些任务。
中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。
2.进行监控
利用线程池提供的参数进行监控,参数如下:
getTaskCount:返回曾计划执行的近似任务总数。因为在计算期间任务和线程的状态可能动态改变,所以返回值只是一个近似值。
getCompletedTaskCount:返回已完成执行的近似任务总数。因为在计算期间任务和线程的状态可能动态改变,所以返回值只是一个近似值,但是该值在整个连续调用过程中不会减少。
getLargestPoolSize:线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
getPoolSize:线程池的线程数量。
getActiveCount:返回主动执行任务的近似线程数。
通过扩展线程池进行监控:继承线程池并重写线程池的beforeExecute(),afterExecute()和terminated()方法,可以在任务执行前、后和线程池关闭前自定义行为。如监控任务的平均执行时间,最大执行时间和最小执行时间等。