线程池的使用
1、Executors.newFixedThreadPool(int)
newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
2、Executors.newSingleThreadExecutor()
newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue一个任务一个任务的执行,一池一线程
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3、Executors.newCachedThreadPool()
newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
* Arrays
* Collections
* Executors
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
//List list = new ArrayList();
//List list = Arrays.asList("a","b");
//固定数的线程池,一池五线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5); //一个银行网点,5个受理业务的窗口
// ExecutorService threadPool = Executors.newSingleThreadExecutor(); //一个银行网点,1个受理业务的窗口
ExecutorService threadPool = Executors.newCachedThreadPool(); //一个银行网点,可扩展受理业务的窗口
//10个顾客请求
try {
for (int i = 1; i <=10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
4、ThreadPoolExecutor底层原理
咱们作为开发者,上面都有开发主管,主管下面带领几个小弟干活,CTO给主管授权说,你可以招聘5个小弟干活,新来任务,如果小弟还不到吴哥,立即去招聘一个来干这个新来的任务,当5个小弟都招来了,再来任务之后,将任务记录到一个表格中,表格中最多记录100个,小弟们会主动去表格中获取任务执行,如果5个小弟都在干活,并且表格中也记录满了,那你可以将小弟扩充到20个,如果20个小弟都在干活,并且存放任务的表也满了,产品经理再来任务后,是直接拒绝,还是让产品自己干,这个由你自己决定,小弟们都尽心尽力在干活,任务都被处理完了,突然公司业绩下滑,几个员工没事干,打酱油,为了节约成本,CTO主管把小弟控制到5人,其他15个人直接被干掉了。所以作为小弟们,别让自己闲着,多干活。
原理:先找几个人干活,大家都忙于干活,任务太多可以排期,排期的任务太多了,再招一些人来干活,最后干活的和排期都达到上层领导要求的上限了,那需要采取一些其他策略进行处理了。对于长时间不干活的人,考虑将其开掉,节约资源和成本。
调用线程池的execute方法处理任务,执行execute方法的过程:
-
判断线程池中运行的线程数是否小于corepoolsize,是:则创建新的线程来处理任务,否:执行下一步 -
试图将任务添加到workQueue指定的队列中,如果无法添加到队列,进入下一步 -
判断线程池中运行的线程数是否小于 maximumPoolSize
,是:则新增线程处理当前传入的任务,否:将任务传递给handler
对象rejectedExecution
方法处理
1、在创建了线程池后,开始等待请求。
2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后 启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕, 再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
5、线程复用、控制最大并发数、管理线程
每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实 现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以 是阻塞的。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
6、线程池的组成
一般的线程池主要分为以下 4 个组成部分:
-
线程池管理器:用于创建并管理线程池 -
工作线程:线程池中的线程 -
任务接口:每个任务必须实现的接口,用于工作线程调度其运行 -
任务队列:用于存放待处理的任务,提供一种缓冲机制
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
ThreadPoolExecutor 的构造方法如下:
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;
}
7、构造方法参数含义
-
corePoolSize:核心线程大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使有其他空闲线程可以处理任务也会创新线程,等到工作的线程数大于核心线程数时就不会在创建了。如果调用了线程池的
prestartAllCoreThreads
方法,线程池会提前把核心线程都创造好,并启动 -
maximumPoolSize:线程池允许创建的最大线程数,此值必须大于等于1。如果队列满了,并且以创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果我们使用了无界队列,那么所有的任务会加入队列,这个参数就没有什么效果了
-
keepAliveTime:多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间,达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止,如果任务很多,并且每个任务的执行时间比较短,避免线程重复创建和回收,可以调大这个时间,提高线程的利用率
-
unit:keepAliveTIme的时间单位,可以选择的单位有天、小时、分钟、毫秒、微妙、千分之一毫秒和纳秒。类型是一个枚举
java.util.concurrent.TimeUnit
,这个枚举也经常使用 -
workQueue:任务队列,被提交但尚未被执行的任务,用于缓存待处理任务的阻塞队列
-
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
-
handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略
8、拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也 塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
-
AbortPolicy : 直接抛出异常,阻止系统正常运行。 -
CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的 任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。 -
DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再 次提交当前任务。 -
DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢 失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际 需要,完全可以自己扩展 RejectedExecutionHandler 接口。
9、Java 线程池工作过程
-
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。 -
当调用 execute() 方法添加一个任务时,线程池会做如下判断: -
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务; -
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列; -
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要 创建非核心线程立刻运行这个任务; -
如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池 会抛出异常 RejectExecutionException。 -
当一个线程完成任务时,它会从队列中取下一个任务来执行。 -
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
10、高并发场景下创建多少线程才合适
创建多少线程合适, 要看多线程具体的应用场景。 一般来说,我们可以将程序分为:CPU密集型程序和I/O密集型程序, 而针对于 CPU密集型程序和I/O密集型程序,其计算最佳线程数的方法是不同的 。
1、CPU密集型程序
对于CPU密集型计算, 多线程本质上是提升多核CPU的利用率, 所以对于一个4核的CPU, 每个核一个线程, 理论上创建4个线程 就可以了, 再多创建线程也只是增加线程切换的成本。 所以, 对于CPU密集型的计算场景, 理论上“线程的量=CPU核数”就是最合 适的。 但是在实际工作中, 一般会将线程数量设置为“CPU核数+1”, 这样的话, 当线程因为偶尔的内存页失效或其他原因导致阻塞 时, 这个额外的线程可以顶上, 从而保证CPU的利用率 。
所以,在CPU密集型的程序中,一般可以将线程数设置为CPU核数+1。
2、I/O密集型程序
对于I/O密集型的程序,最佳的线程数是与程序中CPU计算和I/O操作的耗时比相关。总体来说,可以将其总结为如下的公式。
单核CPU
最佳线程数 = 1 +(I/O耗时/ CPU耗时)
我们令R=I/O耗时 / CPU耗时, 可以这样理解: 当线程A执行IO操作时, 另外R个线程正好执行完各自的CPU计算。 这样CPU的利用率就达到了100%。
多核CPU
多核CPU的最佳线程数在单核CPU最佳线程数的基础上,乘以CPU核数即可,如下所示。
最佳线程数=CPU核数 [ 1 +(I/O耗时/ CPU耗时)]
上述公式计算的结果为最佳理论值,实际工作中还是要通过实际压测数据来找到最佳线程数,将硬件的性能发挥到极致。
原文始发于微信公众号(小薛博客):一文弄懂线程池
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/91849.html