1、线程池的概念
在使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。由于线程池中有很多操作都是与优化资源相关的,这里就不多赘述。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
2、线程池的使用
获取线程池的方式有两种:
- 利用工具类Executors里面的静态方法
- 自己new对象
2.1 创建池子(自动创建线程池不推荐)
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。
(创建的是有上限的线程池,也就是池中的线程个数可以指定最大数量)
public static ExecutorService newCachedThreadPool()
:返回线程池对象。
(很多资料说没有上限,其实不对,上限很大,为int的最大值 下面会分析)
2.2 提交任务
public Future<?> submit(Runnable task)
:将任务提交给线程池
Future接口:用来记录线程任务执行完毕后产生的结果。
如果是用多线程的第三种方式提交任务,那么可以获取线程运行的结果。
2.3 提交后的情况
- 有空闲线程就执行
- 没有空闲线程,但是线程池中的核心线程还没有到达上限,则创建新的线程执行提交的任务
- 没有空闲线程,但是线程池中的核心线程已经到达上限,则排队等待
- 排队的线程有没有数量限制呢?如果排队的任务数量到上限了怎么办?
此时就会创建临时线程。
- 当核心线程都在忙,临时线程也在忙,而且队伍也排满了,那么就会触发任务的拒绝策略。
2.4 使用线程池中线程的步骤
- 创建线程池对象。
- 将要执行的任务交给池子
- 关闭线程池(一般不做)。
Runnable实现类代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);//包含2个线程对象
// 将要执行的任务提交给线程池
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
// 注意:submit方法调用结束后,将使用完的线程又归还到了线程池中
// 销毁线程池,相当于把池子给砸了,在开发中一般不砸
// service.shutdown();
}
}
2.5 源码分析
2.5.1 Executors.newFixedThreadPool 方法分析
Test:
1号池中的线程1,2,3执行完后,经过3秒等待,执行完后,归还线程到线程池中,3秒过后,又从线程池中获取线程,又执行了,thread -1,thread -2,thread -3。
2.5.2 Executors.newFixedThreadPool 方法分析
Executors.newFixedThreadPool 中的参数为:4,这里的4只是最大线程容量为4,很多资料里面写着是初始容量为4,下面是最大容量的验证。
步骤:利用Debug 去分析
发现 pool size 这个属性初始为0,接下来继续执行。
pool size 为 1,使用的线程数为1。
pool size 为 2,使用的线程数为2。
pool size 为 3,使用的线程数为3。
pool size 为 4,使用的线程数为4。
发现在执行第五次的时候, pool size还是为 4,然后继续执行,发现size都是4, 那么下面提交的线程去哪了?其实都进行了队列,下面会分析。
2.5.3 小结
Executors.newFixedThreadPool 与 Executors.newFixedThreadPool 方法底层都是回返了ThreadPoolExecutor 线程池对象。
通过两个方法的源码进去看发现是一个ThreadPoolExecutor的带参构造方法,然后通过API帮助文档,看到ThreadPoolExecutor类有多个不同参数的构造方法。
那么这么多的参数,分别代表什么意思呢?
3、自己创建线程池的对象(推荐使用)
步骤:直接创建ThreadPoolExecutor的对象即可。
参数:
- 参数一:核心线程数量(不能小于0)
- 参数二:最大线程数(不能小于等于0,最大数量 >= 核心线程数量)
- 参数三:空闲线程最大存活时间(不能小于0)
- 参数四:时间单位(时间单位)
- 参数五:任务队列也叫阻塞队列(不能为null)
- 参数六:创建线程工厂,创建线程的方式(不能为null)
- 参数七:任务的拒绝策略,要执行任务过多时的解决方案(不能为null)
分析(用餐厅举例子):
参数一可以理解为:正式员工数量
参数二可以理解为:餐厅最大员工数
参数三可以理解为:临时员工空闲多长时间被辞退(空闲时间值)
参数四可以理解为:临时员工空闲多长时间被辞退(空闲时间单位,秒、分、时)
参数五可以理解为:排队客户
参数六可以理解为:从哪来招人
参数七可以理解为:当排队人数过多,超出顾客请下次再来(拒绝策略)
3.1 队列的种类
队列分为两种,有界和无界
- ArrayBlockingQueue:有界队列,我们在创建对象的时候可以指定上限,也必须指定上限
- LinkedBlockingQueue:无界队列,但是事实上还是有界的,只不过队伍可以很长,int的最大值21亿多
3.2 任务的拒绝策略
分为四种:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,是默认的策略
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 这是不推荐的做法
- ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务 然后把当前任务加入队列中
- ThreadPoolExecutor.CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行。
3.3 临时线程创建时机
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
注意:核心线程在忙,再有任务提交,先排队,队伍满了,再开临时线程。
3.4 触发拒绝策略时机
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝
简单理解:所有的都满了,才会拒绝。
3.5 案例分析
MyThread类
package cn.itxiaoli.pool;
/**
* @author xiaoli
* @className MyThread
* @description:
* @date 2022/3/18 16:19
*/
public class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了");
while (true){
}
}
}
Test类
package cn.itxiaoli.pool;
import java.util.concurrent.*;
/**
* @author xiaoli
* @className Test
* @description:
* @date 2022/3/18 16:19
*/
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// 核心线程数量
3,
// 最大线程的数量
5,
// 空闲时间值
60,
// 空闲时间单位
TimeUnit.SECONDS,
// 指定任务队列,最多为3
new ArrayBlockingQueue<>(3),
// 创建线程的方式,底层就是进行了new Thread
Executors.defaultThreadFactory(),
// 默认的拒绝策略,当队列满了而且线程池中所有的线程都在运行时,再添加任务则会触发
new ThreadPoolExecutor.AbortPolicy()
);
threadPoolExecutor.submit(new MyThread());
threadPoolExecutor.submit(new MyThread());
threadPoolExecutor.submit(new MyThread());
threadPoolExecutor.submit(new MyThread());
threadPoolExecutor.submit(new MyThread());
threadPoolExecutor.submit(new MyThread());
threadPoolExecutor.submit(new MyThread());
threadPoolExecutor.submit(new MyThread());
}
}
3.5.1 Debug
第一次执行时:
pool size = 1 , workQueue=0 ,workQueue为队列
第二次执行时:
pool size = 2 , workQueue=0
第三次执行时:
pool size = 3 , workQueue = 0
第四次执行时:
pool size = 3 , workQueue = 1,队列执行。
第五次执行时:
pool size = 3 , workQueue = 2
第六次执行时:
pool size = 3 , workQueue = 3
第七次执行时:
pool size = 4 , workQueue = 3 ,发现没有运行队列中的地址值没变,也就是说没有运行队列中的任务,而是直接创建了临时线程。
第八次执行时:
pool size = 5 , workQueue = 3 ,池中数量为5
第九次执行时:
程序曝出异常,执行拒绝策略
磊你真猛,我真爱你
人生无常,大肠包小肠