1️⃣ 概述
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
2️⃣Thread
Thread类也是线程类,是Java中线程的基础类,但是它有很多弊端;
① 每次new Thread新建对象,性能差;
② 线程缺乏统一的管理,可能会无限制的新建线程,相互竞争,有可能会占用过多的系统资源导致死机或者OOM;
③ 缺少更多的功能,如更多执行 定期执行 线程中断;
3️⃣线程池的好处
① 重用存在的线程,较少对象的创建以及消亡的开销,性能好;
② 可有效的控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞;
③ 可以提供定期执行 定时执行 单线程 并发数控制等功能;
4️⃣线程池相关的类
1 ThreadPoolExecutor
① 相关参数介绍
corePoolSize : 核心线程数量;
maximumPoolSize : 线程最大线程数;
workQueue : 阻塞队列,存储等待执行的任务(很重要,会带线程池运行过程产生很大的影响);
keepAliveTime : 线程没有任务执行时最多保持多久时间终止;
unit : keepAliveTime的时间单位;
threadFactory : 线程工厂,用来创建线程;
handler : 当拒绝处理任务时的策略;
② 参数关系
首先运行的线程数少于corePoolSize的时候会直接创建新线程来处理任务,即使线程池中的其他线程是空闲,如果线程池中的线程数量大于等于corePoolSize且小于maximumPoolSize的时候,这种情况下只有当workQueue满的时候才会去创建新的线程处理任务;如果设置的corePoolSize与maximumPoolSize相同的话,此时创建的线程池大小是固定的,这个时候如果有新任务提交如果此时的workQueue还没有满,就会把请求放入到workQueue里边,等待空闲的线程从workQueue中取出任务进行处理;如果我们运行的线程数量大于maximumPoolSize时,如果workQueue也是满的情况下,那么将会通过拒绝策略来执行任务;所以我们在任务提交的时候判断的顺序主要是这三个参数;
③ workQueue详细介绍
workQueue是一个保存等待执行任务的阻塞队列,当我们提交一个新的任务到线程池以后,线程池会根据当前线程池中正在运行着的线程的数量来决定该任务的处理方式,处理方式共三种:直接切换 使用无界队列 使用有界队列;
④ workQueue任务的处理方式
直接切换 : 直接切换这种常用的队列就是SynchronousQueue;
无界队列 : 无界队列就是LinkedBlockingQueue,如果使用这种方式线程池中能够创建的最大线程数就是corePoolSize所允许的值,同时maximumPoolSize就不会起作用了,当线程池中所有的核心线程都是运行的状态时,这时一个新的任务提交以后就会放入到等待队列里边;
有界队列 : 底层使用的是ArrayBlockingQueue,使用这种方式可以将线程池的最大线程数限制为maximumPoolSize的值,这样能够降低资源的消耗,但是这种方式它使得线程池对线程的调度非常困难,因为线程池和队列的容量都是有限的,如果先避免以上的问题我们就需要合理的设置参数;
⑤ 参数设置场景
1 如果想要降低系统的消耗(包括CPU的使用率 操作系统资源的消耗 上下文切换的开销等等)可以设置一个较大的队列容量和一个较小的线程池容量,这样会降低线程处理任务的吞吐量;如果我们提交的任务经常发生阻塞,我们可以考虑调用这里边的最大线程数方法来重新设定线程池的容量,如果队列的容量设置的较小,通常需要把这个线程池的容量设置的大一点,这样CPU的使用率会相对的高一些,但是如果线程池容量设置的过大在提交任务的时候并发量会增加,这样有可能会降低处理任务的吞吐量;
5️⃣线程池实例的状态
1 线程池状态图示
2 线程池状态详解
① RUNNING : 此状态可以接受新提交的任务并且也能处理阻塞队列中的任务;
② SHUTDOWN : 关闭状态,不能接受新提交的任务但是可以继续处理阻塞队列中已经保存的任务,当线程池处于RUNNING状态时调用shutdown()方法会使线程池进入到该状态;
③ STOP : 此状态也不能接受新的任务,也不处理队列中的任务,它会中断正在处理任务的线程;当线程处理RUNNING或者SHUTDOWN状态时如果调用了shutdownNow()方法则会进入该状态;
④ TIDYING : 如果所有的任务都已经终止了,这时候我们的有效线程数为0,线程池会进入到该状态,之后如果调用terminated()方法会进入TERMINATED状态;默认的terminated()方法什么都没有做,只是在调用这个方法以后会进入到TERMINATED状态;这些状态我们不需要特别的处理,他是线程池内部自己定义的;
6️⃣线程池提供的方法
1 execute() : 提交任务交给线程池处理;
2 submit() : 提交任务能够返回执行结果,execute + Future;
3 shutdown() : 关闭线程池,等待任务都执行完;
4 shurdownNow() : 关闭线程池,不等待任务执行完;
5 getTaskCount() : 获取线程池已执行和未执行的线程总数;
6 getCompletedTaskCount() : 获取线程池中已完成的任务总数;
7 getPoolSize() : 获取线程池当前的线程数量;
8 getActiveCount() : 获取当前线程池中正在执行任务的线程数量;
7️⃣线程池UML类图
1 Executor接口
它是一个运行新任务的简单接口;
2 ExecutorService接口
它扩展了Executor接口,它添加了一些管理执行器生命周期和任务声明周期的方法;
3 ScheduledExecutorService接口
扩展了ExecutorService接口,它支持Future和定期执行任务;
4 ThreadPoolExecutor
他是线程中功能最强大的,因为我们可以根据自己需要传入需要的参数以及指定任何策略,因此这也是最重要的;
8️⃣Executor框架接口
1 Executors.newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的长度超过了处理的需要可以灵活回收空闲线程,如果没有可以回收的就新建线程;
2 Executors.newFixedThreadPool()
创建一个定长的线程池,可以控制线程的最大并发数,超出的线程会在队列中等待;
3 Executors.newScheduledThreadPool()
创建一个定长的线程池,支持定时以及周期性的任务执行;
4 Executors.newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的一个工作线程来执行任务,保证所有任务按照指定顺序来执行(指定顺序 : 先入先出 优先级等等)
5 总结
以上四种线程池的底层都是调用ThreadPoolExecutor()来创建的,不同的是他们传入的参数不一样;但是使用Executors生成的线程池只是使用起来比较方便但是缺少了很多高级的功能;
9️⃣线程池代码演示
1 Executors.newCachedThreadPool()
@Slf4j
public class ThreadPoolExample1 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
log.info("task:{}", index);
}
});
}
executorService.shutdown();
}
}
2 Executors.newFixedThreadPool()
@Slf4j
public class ThreadPoolExample2 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
log.info("task:{}", index);
}
});
}
executorService.shutdown();
}
}
3 Executors.newScheduledThreadPool()
@Slf4j
public class ThreadPoolExample3 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(new Runnable() {
@Override
public void run() {
log.info("task:{}", index);
}
});
}
executorService.shutdown();
}
}
4 Executors.newSingleThreadExecutor()
@Slf4j
public class ThreadPoolExample4 {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
// executorService.schedule(new Runnable() {
// @Override
// public void run() {
// log.warn("schedule run");
// }
// }, 3, TimeUnit.SECONDS);
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
log.warn("schedule run");
}
}, 1, 3, TimeUnit.SECONDS);
// executorService.shutdown();
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
log.warn("timer run");
}
}, new Date(), 5 * 1000);
}
}
🔟线程池的合理配置
线程池在配置的时候需要根据任务的类型来配置线程池的大小;这里所说的参考值需要大家根据实际情况来进行调整,建议先设置为参考值然后根据任务运行情况 系统负载和cpu利用率进行参数的调整;
1 cpu密集型任务,就需要尽量压榨cpu,参考值可以设为cpu数量 + 1;
2 如果是IO密集型的任务,参考值可以设置为2倍的cpu数量;
我们使用线程池的目的是为了充分复用线程,减少线程的创建于消亡从而提升性能,使用线程池有效的控制最大并发数,可以避免过多的资源竞争导致的阻塞,线程池也可以定时执行单线程等控制线程的执行;
网友评论