一、为什么使用线程池
1)提高性能:系统启动一个新线程的成本是比较高的,而使用线程池避免了频繁地创建和销毁线程,可以很好地提高性能。线程池里的线程结束后并不会死亡,而是回到线程池中称为空闲线程,等待使用;
2)控制线程数量:使用线程池还可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数量。
二、一个使用线程池的简单案例
步骤:
1)调用Executors类的静态方法创建一个ExecutorService对象,该对象代表一个线程池;
2)创建Runnable实现类或Callable实现类的实例,作为线程执行任务;
3)调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;
4)当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池;
public class ThreadPoolTest{ public static void main(String[] args){ //1.创建线程池 ExecutorService pool = Executors.newFixedThreadPool(6); //2.创建要执行的线程任务 MyThread mt1 = new MyThread(); MyThread mt2 = new MyThread(); //3.提交线程任务 pool.submit(mt1); pool.submit(mt2); //4.关闭线程池 pool.shutdown(); } } class MyThread implements Runnable{ public void run(){ for(int i = 0; i < 100; i++){ System.out.println("MyThread--" + "i"); } } }
三、深入了解线程池的参数
ThreadPoolExecutor是java线程池框架(Executor-> ExecutorService)的主要实现类,它的构造函数参数如下:
public ThreadPoolExecutor( int corePoolSize, //核心线程的数量
int maximumPoolSize, //最大线程数量
long keepAliveTime, //超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间的单位
BlockingQueue<Runnable> workQueue, //保存待执行任务的队列
ThreadFactory threadFactory, //创建新线程使用的工厂
RejectedExecutionHandler handler // 当任务无法执行时的处理器 )
对线程池的参数的介绍:
1)corePoolSize:核心线程池数量 。在线程数少于核心数量时,有新任务进来就新建一个线程,即使有的线程没事干;等超出核心数量后,就不会新建线程了,空闲的线程就得去任务队列里取任务执行了。
2)maximumPoolSize:最大线程数量 ,包括核心线程池数量 + 核心以外的数量。
如果任务队列满了,并且池中线程数小于最大线程数,会再创建新的线程执行任务。
3)keepAliveTime:核心池以外的线程存活时间,即没有任务的外包的存活时间
如果给线程池设置 allowCoreThreadTimeOut(true),则核心线程在空闲时头上也会响起死亡的倒计时
如果任务是多而容易执行的,可以调大这个参数,那样线程就可以在存活的时间里有更大可能接受新任务
4)workQueue:保存待执行任务的阻塞队列 。线程池中使用的队列是 BlockingQueue
接口,常用的实现有如下几种:
a)ArrayBlockingQueue:基于数组、有界,按 FIFO(先进先出)原则对元素进行排序;
b)LinkedBlockingQueue:基于链表,按FIFO (先进先出) 排序元素;
- 吞吐量通常要高于 ArrayBlockingQueue
- Executors.newFixedThreadPool() 使用了这个队列
c)SynchronousQueue:不存储元素的阻塞队列;
- 每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
- 吞吐量通常要高于 LinkedBlockingQueue
- Executors.newCachedThreadPool使用了这个队列
d)PriorityBlockingQueue:具有优先级的、无限阻塞队列。
5)threadFactory:每个线程创建的地方。可以给线程起个好听的名字,设置个优先级等。
6)handler:饱和策略,大家都很忙,咋办呢,有四种策略 。
CallerRunsPolicy:只要线程池没关闭,就直接用调用者所在线程来运行任务
AbortPolicy:直接抛出 RejectedExecutionException 异常
DiscardPolicy:悄悄把任务放生,不做了
DiscardOldestPolicy:把队列里待最久的那个任务扔了,然后再调用 execute() 试试看能行不
四、线程池的处理流程
线程池具体的执行方法ThreadPoolExecutor.execute:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); //1.当前池中线程比核心数少,新建一个线程执行任务 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //2.核心池已满,但任务队列未满,添加到队列中 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) //如果这时被关闭了,拒绝任务 reject(command); else if (workerCountOf(recheck) == 0) //如果之前的线程已被销毁完,新建一个线程 addWorker(null, false); } //3.核心池已满,队列已满,试着创建一个新线程 else if (!addWorker(command, false)) reject(command); //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务 }
线程池的处理流程图:
五、JDK 提供的线程池及使用场景
1.java.util.concurrent.Executors是JDK自带的创建线程的工具类,其创建的线程池是ThreadPoolExecutor的实例。Executors创建线程池比较典型的4种方式:
1) newFixedThreadPool:创建一个核心线程个数和最大线程个数都为nThreads的线程池,并且阻塞队列长度为Integer.MAX_VALUE,keeyAliveTime=0说明只要线程个数比核心线程个数多并且当前空闲则回收。可能引发的问题:阻塞队列太大会引起OOM;核心线程会常驻内存,可能会造成资源浪费。
2)newSingleThreadExecutor:创建一个核心线程个数和最大线程个数都为1的线程池,并且阻塞队列长度为Integer.MAX_VALUE,keeyAliveTime=0说明只要线程个数比核心线程个数多并且当前空闲则回收。缺点:单线程,可能会堆积大量待执行的任务。
3)newCachedThreadPool: 创建一个按需创建线程的线程池,最大线程数为Integer.MAX_VALUE,阻塞队列为最多只有一个任务的同步队列,keeyAliveTime=60只要线程60s内空闲则回收。问题点:最大线程数太大会引起OOM。
4)newScheduledThreadPool:创建一个最小线程个数corePoolSize,最大为Integer.MAX_VALUE,阻塞队列为DelayedWorkQueue的线程池。
2.如何选择使用哪个JDK 提供的线程池?
- CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
- FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。
- SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
- ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
3.注意:Executors创建线程的方式虽然简单方便,但并不推荐使用此种方式。建议使用new ThreadPoolExecutor(...)的方式创建线程池。
六、两种提交任务的方法
ExecutorService 提供了两种提交任务的方法:
execute():提交不需要返回值的任务
submit():提交需要返回值的任务
七、线程池使用注意事项
1)建议使用new ThreadPoolExecutor(...)的方式创建线程池:
线程池的创建不应使用 Executors 去创建,而应该通过 ThreadPoolExecutor 创建,这样可以让读者更加明确地知道线程池的参数设置、运行规则,规避资源耗尽的风险,这一点在也阿里巴巴JAVA开发手册中也有明确要求。
2)合理设置线程数:
线程池的工作线程数设置应根据实际情况配置,CPU密集型业务(搜索、排序等)CPU空闲时间较少,线程数不能设置太多。N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。
3)设置能代表具体业务的线程名称:这样方便通过日志的线程名称识别所属业务。具体实现可以通过指定ThreadPoolExecutor的ThreadFactory参数。如使用spring提供的CustomizableThreadFactory。
参考文献:
1)https://www.cnblogs.com/gdpdroid/p/4128177.html
2)https://blog.csdn.net/u011240877/article/details/73440993
3)https://baijiahao.baidu.com/s?id=1595521551376533549&wfr=spider&for=pc
原文:https://www.cnblogs.com/xy-ouyang/p/10466546.html