为什么使用多线程 ?
- 以前 : CPU 单核电脑 , CPU 通过快速的切换 , 只在视觉上感觉是在同时运行不同的任务
- 现在 : 多核电脑 , 多个线程 各自跑在独立的 CPU 上 , CPU 不用切换 , 效率高
线程池的优势 :
线程池
用来控制运行的线程数量 , 处理过程中将任务放入队列 , 在线程创建后 启动这些任务 , 如果线程的数量超过了最大数量 , 超出数量的线程排队等候 , 等其他线程执行完毕, 再从队列中取出任务执行
特点 : 线程复用 , 控制最大并发数, 管理线程
1. 降低资源消耗 , 通过重复利用已创建的线程, 降低线程的创建和销毁造成的开销
2. 提高响应速度 , 当任务到达时, 任务可以不需要等待创建线程就能立刻执行
3. 提高线程的可管理性 , 线程是稀缺资源 , 如果无限制的创建 , 不仅会消耗系统的资源 , 还会降低系统的稳定性 , 使用线程池 可以进行统一的分配, 调优监控
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类
线程池如何使用
1 . Executors.newFixedThreadPool ( int )
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue
2 . Executors.newSingleThreadExecutor ( )
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue
3 . Executors.newCachedThreadPool ( )
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,
//它使用的是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
注意 : 我们可以发现 他们底层都使用的 ThreadPoolExecutor 这个类
ThreadPoolExecutor 底层原理
线程池几个重要参数 ( 7大参数 )
线程池的七大参数
- corePoolSize : 线程池中的常驻核心线程数
- maximunPoolSize : 线程池中能够容纳同时执行的最大线程数, 此值必须大于1
- KeepAliveTime : 多余的空闲线程的存活时间 , 当前池中线程数量超过 corePoolSize 时 , 当空闲时间达到 KeepAliveTime时 , 多余的线程会被摧毁掉 , 只剩下corePoolSize个线程为止
- unit : keepAliveTime 的单位
- workQueue : 任务队列 , 被提交但尚未被执行的任务
- threadFactory : 表示生成线程池中工作线程的线程工厂 , 用于创建线程 , 一般默认的即可
- handler : 拒绝策略, 当队列满了 , 并且工作线程大于等于线程池的最大线程数时 , 如何来拒绝请求执行的 runnable 策略
泳道图 :
线程池的工作流程
- 在创建了线程后, 开始等待请求
- 当调用 execute ( ) 方法添加一个请求任务时 线程池会做出如下判断 :
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务.
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;注意 : 此时并不会立即扩容创建非核心线程 , 可以看出 阻塞队列相当于缓冲区 , 减小 CPU 的压力.
- 如果这个时候队列也满了 , 且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这些任务.
- 如果 队列满了 且 正在运行的线程数量大于或等于maximumPoolSize ( 线程池也满了 ) ,那么线程池会 启动饱和拒绝策略来执行.
- 当一个线程完成任务时,它会从队列中取下一个任务来执行.
- 当一个线程无事可做超过一定的时间( keepAliveTime )时,线程会判断 :
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉.
- 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小.
线程池的拒绝策略
等待队列已经满了 , 再也塞不下新任务了 , 同时 线程池中的 max 线程也达到了 , 无法继续为新任务服务......
这个是时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略
- AbortPolicy (默认):直接抛出 RejectedExecutionException 异常 阻止系统正常运行
- 异常 : java并发包的线程池拒绝策略异常
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- 就是说 : 我一共就有5个线程 , 你来了10个 , 我处理不过来 , 没有处理的任务就原路返回 , 注意 : 也是有概率处理完的
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
在工作中 单一的 / 固定数的 / 可变的 这三种创建线程池的方法哪个用的多?
答案是一个都不用,我们工作中只能使用 自定义的
Executors 中JDK已经给你提供了,为什么不用? ( Out Of Memory )
如何自定义线程池
根据上面的三个创建线程池的方式 , 我们可以发现 底层都是new了一个叫做 ThreadPool Executor 的线程池对象 , 那么 我们直接自己 new一个 ThreadPool Executor 的线程池对象 , 然后根据业务的需求 , 指定合理的参数就可以了
package com.threadpool;
import java.util.concurrent.*;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 1; i <= 8; i++) {
final int tmpI = i;
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + "\t 处理业务" + "\t 顾客: " + tmpI);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
生产中如设置合理线程池大小的设置
根据任务类型来配置线程池大小
如果是 CPU 密集型任务
- 处理复杂的逻辑 和 算法的操作 , 可能要处理很久 非常消耗CPU资源
如果是 CPU 密集型任务,那么就意味着 CPU 是稀缺资源,这个时候我们通常不能通过增加线程数来提高计算能力,因为线程数量太多,会导致频繁的上下文切换,一般这种情况下,
建议合理的线程数值是 N(CPU)数 + 1。如果是八核就配置9 ,避免CPU告高速切换
如果是 I/O 密集型任务
- 简单的业务逻辑处理 , 但是数量巨大 , CPU 的消耗很少 , 大部分都是在等待读写操作
如果是 I/O 密集型任务,就说明需要较多的等待,这个时候可以参考 Brain Goetz 的推荐方法
线程数 = CPU核数 × (1 + 平均等待时间/平均工作时间)。
参考值可以是 N(CPU) 核数 * 2。
具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整