目录
1.池的引入介绍
池,在计算机中很常见。
1.1.什么是池
(1)池举例
像我们之前已经接触过的:常量池,现在准备学习的线程池等等。像:常量池、线程池、数据库连接池、进程池、内存池等等,它们之间用到的思想都是差不多的。
(2)使用池的目的
目的:提高效率
(3)如何提高效率
相比大家对“养鱼”这个词语都不陌生吧,它不是普通的养鱼,而“养鱼”的人就称为“海王”。
而“养鱼”就需要一个池,暂且称它为鱼池。当一个海王不想要这条鱼时,就可以直接丢弃,并且可以直接从鱼池中再选取一条鱼。如果没有池,那么丢弃之后,就要花费各种时间和精力再去寻找下一条鱼。因此,池是可以提高换鱼的效率的。
1.2.线程池的优势
(1)线程池出现的原因
即使可以多线程编程,但是如果线程反复不断的创建和销毁,这些小的花销积攒起来也是不小的,当所谓,时间久了就会感到厌倦。
为了对上述情况进行优化,有两种做法:线程池和协程(纤程),这里只介绍线程池
(2)线程池可以提高效率的原因
1)直接调用api去创建线程/销毁线程,是需要通过 用户态+内核态 配合去完成的,而内核中的许多操作都重量级的。
2)通过线程池/协程,则只需要通过用户态就可以完成,不需要通过内核态。因此使用线程池之后,效率会得到提高。
3)在使用线程吃的时候,线程都是已经提前创建好并且使用数据结构存放在用户态中;当需要使用的时候,可以直接取出来,使用完再存放回去。
2.线程池参数介绍和使用
2.1.线程池的七个参数
标准库线程池的类:ThreadPoolExecutor,有四个构造方法,下面介绍构造方法中参数最多的一个。(七个构造方法、第七个最重要)
上述的某些参数,不单独介绍,需要组合起来一起介绍
(1)第一、二个参数
1)corePoolSize:表示核心线程数
2)maximumPoolSize:表示最大线程数(最大线程数=核心线程数+非核心线程数)
3)线程池的动态扩展
当线程池刚创建好的时候,里面就会包含n个核心线程,此时,线程池中的线程数也就是n。
而线程池会提供一个submit方法,就可以往线程池中添加任务(Runnable对象);当任务少时,不需要多添加其他的线程,但是当任务多时,此时的n个线程处理不过来了,这个时候,线程池就会自动创建出新的线程来(创建的线程总数 <= 最大线程数),这个线程就是非核心线程;当任务少时,就会把这些非核心线程给回收了。
4)线程数的设计
在实际开发中,线程数不可无脑设计,需要根据实际的情况来。(电脑的配置和cpu核心数等等)
两个极端情况:
1.cpu密集型程序,完成代码需要消耗很多的cpu。此时的线程数目应该<=cpu逻辑核心数。
2.IO密集型程序,不需要很多cpu。此时,线程数可以任意多
但是实际的情况,都是介于两者之间的,所以正确的做法是:
通过实验的方式,去测试性能最高的一个值。
注意:当线程池中的线程数目过多时(甚至超过cpu逻辑核心数),不会出现问题,但是会增加线程调度的开销。因为,cpu逻辑核心数就那么多,这么多线程,就需要去等待,也就是调度。
(2)第三、四个参数
1)keepAliveTime:非核心线程运行空闲的最大时间(超过就会被回收)
2)unit:上面的时间单位
这些单位有很多种:
(3)第五个参数
1)workQueue:任务队列
2)作用:用于保存Runnable任务,供线程去执行(会在submit方法中进行添加任务)
(4)第六个参数
1)threadFactory:工厂类
2)作用:用于创建线程
3)这个参数可以不提供
(5)第七个参数
1)handler:拒绝策略
2)作用:当任务队列满时,线程池该怎么做?
3)下面四种做法(策略)
1.ThreadPoolExecutor.AbortPolicy:直接抛出异常。
2.ThreadPoolExecutor.CallerRunsPolicy:线程池不执行该任务,谁添加的任务,就由谁来执行
3.ThreadPoolExecutor.DiscarOldestPolicy,:丢弃最老的任务(队头任务),执行新的任务
4.ThreadPoolExecutor.DiscardPolicy,:丢弃最新的任务(正在submit的任务),然后按照原有方式执行
2.2.线程池的工作流程
这里是介绍在有了线程池之后的。
(1)当向线程池中添加任务时,线程池会先检查是否有空闲的线程。如果有空闲线程,就会给该任务分配一个空闲的线程。
(2)如果没有空闲的线程,就会检查当前线程数是否小于核心线程数;如果小于,线程池会创造一个新的核心线程来执行任务。
(3)如果线程数>=核心线程数,线程池会检查工作队列是否已满;如果工作队列未满,新任务会被放入工作队列中等待执行。
(4)如果工作队列已满,检查当前线程数是否小于最大线程数;如果小于,线程池就会创建新的非核心线程来执行任务。
(5)如果当前线程数已达到最大值,线程池就会采取拒绝策略。
以上的五个过程是一个动态的过程。下面是大致的流程图
2.3.使用现有线程池
因为ThreadPoolExecutor类的构造方法,直接使用起来比较的复杂,所以标准库中自己提供了几个工厂类,对该线程池进行了进一步的封装(也就是使用了工厂模式对构造进行封装,初始化就不需要调用构造方法,直接调用工厂类的静态方法即可)
(1)工厂类
Executors
(2)工厂类提供可以创建线程池的四个方法
//1.创建最普通的线程池、可以根据任务的数目。自动进行线程扩容
Executors.newCachedThreadPool();
//2.创建固定数目的线程池
Executors.newFixedThreadPool(10);
//3.创建一个只含单个线程的线程池
Executors.newSingleThreadExecutor();
//4.创建一个固定线程个数,但是任务延时执行的线程池
Executors.newScheduledThreadPool(10);
第一、第二个比较常用;而前面两个中第一个是无线创建线程,容易消耗更多的cpu资源,为了更安全等因素,我们推荐使用第二个可以指定线程数的方法。
(3)接收返回值
工厂调用返回的类型是ExecutorService类型的
ExecutorService service = Executors.newFixedThreadPool(10);
(4)使用线程池编程
创造好线程池之后,就需要向线程池中添加Runnable任务,当有任务之后,线程就会马上执行。
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 5000; i++) {
int count = i;
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("第"+count+"个任务"+" name: "+Thread.currentThread().getName());
}
});
}
}
运行结果:
3. 模拟实现线程池
现在,我们要自己模拟实现一个线程池。先明确一个线程池所需要的东西:
(1)n个线程-用于执行任务(2)任务队列-用于存放任务(3)submit方法-用于向线程池中提交任务
而这里模拟的线程池,是被工厂类封装起来的。
3.1.实现最大核心数线程池
(1)线程池需要的框架
(2)完成线程池
class MyThreadPool {
//1.有任务队列
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);//任务队列可以存放1000个Runnable任务
//2.构造方法,用于初始化
public MyThreadPool(int morePoolSize) {//n就是要初始化多少个线程
for (int i = 0; i < morePoolSize; i++) {//下面根据核心线程数创造
Thread t = new Thread(()->{
while (true) {//线程创建后之后,就不断的去执行任务,否则只执行一次任务就会被销毁了
try {
Runnable runnable = queue.take();//从任务队列中取出任务
runnable.run();//执行run中的任务
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
threadList.add(t);
}
}
//3.提供submit方法
void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}
1)构造方法中,可以执行最大的线程数;当创建对象时,线程就已经被创建好了,当任务队列为空时,这些线程就会阻塞等待,直到有任务。
2)submit方法,只需要向任务队列中添加任务即可
测试代码:
public static void main(String[] args) throws InterruptedException {
MyThreadPool threadPool = new MyThreadPool(10);//核心线程、最大线程
for (int i = 0; i < 101; i++) {//循环向线程池中插入任务
int id = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("我是第"+id+"个任务"+Thread.currentThread().getName());
}
});
}
}
运行结果:
上面是指定只有核心线程数的,下面多加一个最大线程数
3.2.使用最大线程数线程池
(1)改进1:构造方法多一个参数,然后存于成员变量maxPoolSize中
(2)改进2:提供一个顺序表,用于存储已有线程
(3)改进3:在submit方法中进行判断,当已有任务到达一定的数量并且已有线程数还可以增加时,就多创建一些线程出来进行处理任务。
(4)代码改进:
class MyThreadPool {
//1.有任务队列
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);//任务队列可以存放1000个Runnable任务
private int maxPoolSize = 0;
private List<Thread> threadList = new ArrayList<>();//用于存放已经创建好的线程
//2.构造方法,用于初始化
public MyThreadPool(int morePoolSize,int maxPoolSize) {//n就是要初始化多少个线程
this.maxPoolSize = maxPoolSize;//记录最大的线程数
for (int i = 0; i < morePoolSize; i++) {//下面根据核心线程数创造
Thread t = new Thread(()->{
while (true) {//线程创建后之后,就不断的去执行任务,否则只执行一次任务就会被销毁了
try {
Runnable runnable = queue.take();//从任务队列中取出任务
runnable.run();//执行run中的任务
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
threadList.add(t);
}
}
//3.提供submit方法
void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
//当任务数到达一定的数量时,自动增加线程
if(queue.size() >= 500 && threadList.size() < maxPoolSize) {
Thread t = new Thread(()->{
while (true) {//线程创建后之后,就不断的去执行任务,否则只执行一次任务就会被销毁了
try {
Runnable runnable2 = queue.take();//从任务队列中取出任务
runnable.run();//执行run中的任务
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
threadList.add(t);
}
}
}
运行结果和上述是一样的
这就模拟出了线程池。