线程池:
有效的文章线程池
文章目录
前言:
阻塞队列
FIFO
写入: 如果队列满了.就必须阻塞等待
取: 如果队列为空,必须阻塞等待生产
阻塞队列: 多线程并发处理,线程池
四组API
方式 | 抛出异常 | 有返回值.不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer() | put() | offer(,) |
移除 | remove | poll() | take() | poll(,) |
检测队首元素 | element | peek() |
SynchronousQueue同步队列
没有容量
进去一个元素,必须等待取出来之后,才能再往里放一个元素!
put了一个元素,必须从里面先take取出来,否则不能在put进去值!
put tack
线程池:
四大方法,七大参数,四种拒绝策略
什么是线程池:
线程池和数据库连接池非常类似,可以统一管理和维护,减少没有必要的开销
为什么要使用线程池
因为频繁的开启线程或者停止线程,线程需要从新的被CPU从就绪到运行状态调整,需要发生上下文切换,效率非常低
什么是上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
那些地方使用到线程池
实际来发中,禁止以及NEW线程,必须使用线程池来维护和创建线程
线程池有那些作用
核心点: 复用机制,提前创建固定的线程一直子啊运行状态,实现复用…限制线程创建的数据
- 降低资源消耗: 通过线程池化技术重复利用已创建的线程,降低线程创建和销毁造成的消耗
- 提高响应速度任务到达时,无需等待线程创建即可执行
- 提高线程的可管理: 线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因 为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分 配、调优和监控
- 提供强大的功能,线程池具备可扩展性,允许开发人员向其中增加更多的功能,比如延时定时线程池
ScheduledThreadPoolExecutor
,就允许任务延期执行或定期执行。
线程池的四大创建方式
Executors.newCachedThreadPool()
; 可缓存线程池
Executors.newFixedThreadPool()
;可定长度 限制最大线程数
Executors.newScheduledThreadPool()
; 可定时
Executors.newSingleThreadExecutor()
; 单例
底层都是基于 ThreadPoolExecutor
为什么阿里巴巴不建议使用 Executors
因为默认的Executors
线程池底层是基于 ThreadPoolExecutor
构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务容易发生内存溢出,会导致我们最大的线程数会失效
阿里开发手册规定
线程池底层是如何实现复用的
本质思想: 创建一个线程,不会立马停止或者销毁而是一直的实现复用
- 提前创建固定大小的线程一直保持,在允许状态,(可能会非常的消耗cpu资源)
- 当需要线程执行任务,将该任务提交缓存在并发队列中,如果缓存队列满了,则会执行拒绝策略
- 正在运行的线程从并发队列中获取任务执行从而实现多线程复用问题
核心 复用机制
- 提前创建好固定的线程一直在运行状态
- 提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行
- 正在运行的线程就从队列中获取该任务执行
ThreadPoolExecutor
七大核心参数
- int corePoolSize,//核心线程池大小
- int maximumPoolSize,//最大核心线程池大小
- long keepAliveTime,//超时了没有人调用就会释放
- TimeUnit unit,//超时单位
- BlockingQueue<Runnable> workQueue,//阻塞队列
- ThreadFactory threadFactory,//线程工厂:创建线程的,一般不用动
- RejectedExecutionHandler handler//拒绝策略
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池底层ThreadPoolExecutor底层实现的原理
-
当线程数小于核心线程数时,创建线程
-
当线程数大于等于核心线程数时,且任务队列未满时,将任务放入任务队列
-
当线程数大于等于核心线程数,且任务队列已满
3.1 若线程数小于最大线程数,创建线程
3.2 若线程数等于最大线程数,抛出异常,拒绝任务
实际上最多执行的任务 核心线程数+缓存队列容量+(最大线程数-核心线程数)
也就是 缓存队列容量+最大线程数
package com.nie.juc.pool;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
// Executors 工具类、3大方法
/**
* new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
*/
public class Demo01 {
public static void main(String[] args) {
// 自定义线程池!工作 ThreadPoolExecutor
// 最大线程到底该如何定义
// 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
// 2、IO 密集型 > 判断你程序中十分耗IO的线程,
// 程序 15个大型任务 io十分占用资源!
// 获取CPU的核数
System.out.println("CUP核数"+Runtime.getRuntime().availableProcessors());
List list = new ArrayList();
ExecutorService threadPool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),
4,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()); //队列满了,尝试去和最早的竞争,也不会抛出异常!
try {
// 最大承载:Deque + max
// 超过 RejectedExecutionException
for (int i = 1; i <= 8; i++) {
final int finalI=i;
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok"+ " "+finalI);
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
四种拒绝策略
四种拒绝策略详解
线程池队列满了,任务会丢失吗
-
AbortPolicy 丢弃任务,抛出运行时异常
-
CallerRunsPolicy 执行任务
-
DiscardPolicy 忽视,什么都不发生
-
DiscardOldestPolicy 从队列中移除最先进入队列(最后一个执行)的任务
-
实现RejectedExecutionHandler 接口,可自定义处理器
/** *newThreadPoolExecutor.AbortPolicy()//银行满了,还有人进来,不处理这个人的,抛出异常 *newThreadPoolExecutor.CallerRunsPolicy()//哪来的去哪里! *newThreadPoolExecutor.DiscardPolicy()//队列满了,丢掉任务,不会抛出异常! *newThreadPoolExecutor.DiscardOldestPolicy()//队列满了,尝试去和最早的竞争,也不会 抛出异常! */
线程池如何合理配置参数
首先线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
正是因为线程池的这些特点,当我们需要初始化一个线程池时,就要考虑我们的线程池被用来执行什么样的任务。
常见的任务分为两种:CPU密集型任务和IO密集型任务
CPU密集型任务(CPU-bound):
CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。CPU 密集任 务只有在真正的多核 CPU 上才可能得到加速(通过多线程),而在单核 CPU 上,无论你开几 个模拟的多线程该任务都不可能得到加速,因为 CPU 总的运算能力就那些。
在一个任务中,主要做计算,CPU持续在运行,CPU利用率高,具有这种特点的任务称为CPU密集型任务。
IO密集型任务(IO-bound):
在一个任务中,大部分时间在进行I/O操作,由于I/O速度远远小于CPU,所以任务的大部分时间都在等待IO,CPU利用率低,具有这种特点的任务称为IO密集型任务。
两种任务的线程数设置如下
所以我们在设计线程池时,应先对执行的任务有个大体分类,然后根据类型进行设置。一般而言,两种任务的线程数设置如下:
CPU密集型任务:
线程个数为CPU核数。这几个线程可以并行执行,不存在线程切换到开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗
IO密集型:
线程个数为CPU核数的两倍。到其中的线程在IO操作的时候,其他线程可以继续用cpu,提高了cpu的利用率
查看 CPU 核数
System.out.println(Runtime.getRuntime().availableProcessors());