目录
🚩什么是线程池
🎈从池子中取效率大于新创建线程效率(why)
🚩标准库中的线程池
🎈为什么调用方法而不是直接创建对象
🎈工厂类里的方法
📝newCachedThreadPool()
📝newFixedThreadPool()
🚩实现线程池
🚩ThreadPoolExecutor
🎈ThreadPoolExecutor的构造器参数
🎈ThreadPoolExecutor的新任务拒绝策略
🚩什么是线程池
首先我们想象一个场景就是比较渣的场景,我是一个男生,很帅并且有才华,追问的人呢,很多,排成了一个长队。然后我就挑了一个既好看又有钱又有才华的女生进行交往,交往一段时候后,我腻歪了,想换个女朋友,此时我要做俩个事情:1>想办法分手,2>再找一个小哥哥,培养感情。此时进行这俩个操作的时候,效率是很低的,有没有办法优化呢?
优化:我和这个女生A再交往的过程中,同时再和另一个女生B搞暧昧(培养感情),当我想和女生A分手的时候,就只要分手了后我们就可以和女生B直接在一起了。(此时我和女生B感情是有一定的基础了)。此时女生B就是我们所说的备胎。
进一步优化:我需要更高的效率的话,更换女朋友,就可以再和女生A在一起的时候,同时和女生B,C,D交往联络感情,此时女生B,C,D都是我的备胎,此时备胎就构成了——备胎池。
所以和线程池有同样的方式,线程池顾名思义就是存放线程的池子,等需要了就直接调用了,省去了启动和销毁的损耗了。
线程池最大的好处就是减少每次启动、销毁线程的损耗
从上面线程池我们知道,等需要了就直接从线程池中取,但是为什么从池子取得效率比新创建线程得效率更高呢?
🎈从池子中取效率大于新创建线程效率(why)
- 从池子中取,这个动作,是纯粹用户态得操作
- 创建新的线程,这个动作,是需要用户态+内核态相互配合完成的操作。
如果一段程序,是在系统内核中执行,此时就称为"内核态",如果不是,则称为"用户态".
操作系统,是由 内核+配套 的应用程序构成的,内核则是 系统最核心的部分,创建线程操作,就需要调用系统api,进入内核中,按照内核态的方式来完成一系列操作。
场景:
滑稽老哥去银行存钱,但是需要身份证复印件,但是滑稽老哥没有,所以滑稽老哥就有俩个选择。
- A:银行柜员说:你可以给身份证给我,我去帮你打印
- B:银行柜员又说: 大厅的角落,有一个自助复印机,你可以自行去复印。
A这个过程就是涉及到了内核态操作了,所谓内核态就是柜员要进行的操作,此时你交给柜员后,柜员会在给你复印件之前会做哪些工作(因为操作系统内核是给所有的进程提供服务的,当你要创建线程的时候,人家内核会帮你做,但是做的过程,难免会做一些其他的事情)——不可控
B这个过程就是纯粹用户态的操作,所谓用户态就是用户自己要进行的操作,滑稽老哥就可以立即去复印,复印完了之后就立即回来,整个过程中,没有任何拖泥带水的。——可控
🚩标准库中的线程池
- 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
- 返回值类型为 ExecutorService
线程池对象不是直接创建出来的,而是通过一个专门的方法,返回一个线程池对象。
ExecutorService service= Executors.newCachedThreadPool();
- 通过 ExecutorService.submit 可以注册一个任务到线程池中
Executors.newCachedThreadPool();其实是个工厂模式(设计模式),也就是Executors是个工厂类,需要创建线程,但是为什么调用方法呢?而不是直接创建线程池对象呢?
🎈为什么调用方法而不是直接创建对象
创建对象的同时,new关键字就会触发类的构造方法,但是构造方法,存在一定的局限性。
考虑有个类,我们期待用笛卡尔坐标系,来构造对象,又或者用极坐标构造对象,写在一起的时候,这俩个方法是无法重载的(也就是说在一个类中我们要实现不同方式的初始化),就会编译失败。其实很多时候,构造一个对象,希望有多种构造方式,多种方式,就需要使用多个版本的构造方法来分别实现,但是构造方法要求方法的名字必须是类名,不同的构造方法,就只能通过 重载 的方式区分了。(重载是方法名相同,参数个数类型不同),使用工厂模式/设计模式,就能解决这个问题,使用普通的方法,代替构造方法来完成初始化工作,普通方法就可以通过方法名的不同来进行区分了,不必因为重载的规则而限制了。
通过这种,我们通过一个工厂类Executors调用方法创建不同类型的初始化工作。Executors是工厂类,那么调用的方法是工厂方法,然后加工好之后,返回的是整个加工好的线程,而ExecutorService就是线程池,是由工厂类调用工厂方法创建好的。
实践中,一般单独搞一个类,给这个类搞一些静态方法,由这样静态方法负责构造出对象
class PointFactory{public static Point makePointByXY(int x,int y){};public static Point makePointByRA(int R,int A){};
等到需要调用哪个的时候,我们就可以通过类来调用方法。
🎈工厂类里的方法
Executors 创建线程池的几种方式
newFixedThreadPool: 创建固定线程数的线程池
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池.
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装.
📝newCachedThreadPool()
ExecutorService service= Executors.newCachedThreadPool();
newCachedThreadPool()方法中cached缓存,用过之后不着急释放,先留着以备下次使用(此时构造出的线程池对象,有一个基本特点,线程数目是能够动态适应的)随着往线程池中添加任务,这个线程池中的线程会根据需要自动被创建出来,创建出来之后也不会着急销毁,会在池子里保留一定的时间,以备随时再使用。
📝newFixedThreadPool()
ExecutorService service1=Executors.newFixedThreadPool(4);
固定的,指定创建几个线程。具体需要创建几个线程,正确做法就是使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目,看哪种情况下,最符合你的要求。
还有些工厂方法了解即可。
🚩实现线程池
- 核心操作为 submit, 将任务加入线程池中
- 使用 MyThread 类描述一个工作线程. 使用 Runnable 描述一个任务.
- 使用一个 BlockingQueue 组织所有的任务
- 每个 t 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
package ThreadPool;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;class MyThread{BlockingQueue<Runnable> queue=new ArrayBlockingQueue<Runnable>(1000);public void submit(Runnable runnable){queue.offer(runnable);}public void takeTask(int n){for (int i = 0; i < n; i++) {Thread t=new Thread(()->{try {Runnable runnable=queue.take();runnable.run();} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}}
}
public class ThreadPool_test {public static void main(String[] args) {MyThread myThread=new MyThread();for (int i = 0; i <100; i++) {//一百个任务myThread.submit(new Runnable() {@Overridepublic void run() {System.out.println("我爱zyf");}});}myThread.takeTask(10);//10个线程执行100个任务}
}
打印了十个,10个线程执行了10个任务,因为里面没有用while(true)循环,一个线程执行完任务之后就结束了。但是这些线程是可能同时执行各自的任务,但是一个线程肯定是执行一个任务。
🚩ThreadPoolExecutor
在阿里巴巴手册中有一条建议:
【强制】线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
如果经常基于Executors提供的工厂方法创建线程池,很容易忽略线程池内部的实现。特别是拒绝策略,因使用Executors创建线程池时不会传入这个参数,直接采用默认值,所以常常被忽略。
ThreadPoolExecutor可以实现线程池的创建。ThreadPoolExecutor相关类图如下:
从类图可以看出,ThreadPoolExecutor最终实现了Executor接口,是线程池创建的真正实现者。
ThreadPoolExecutor核心方法有俩个,一个是构造方法,一个是注册任务(添加方法).
🎈ThreadPoolExecutor的构造器参数
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
- 参数一
- 指定线程池的线程数量(核心线程): corePoolSize,不能小于0;
- 参数二
- 指定线程池可支持的最大线程数: maximumPoolSize,最大数量 >= 核心线程数量;
- 参数三
- 指定临时线程的最大存活时间: keepAliveTime,不能小于0;(则表示实习生可以摸鱼的时间,并不代表一摸鱼就被开除了)
- 参数四
- 指定存活时间的单位(秒、分、时、天): unit,时间单位;
- 参数五
- 指定任务队列: workQueue,不能为null;
- 参数六
- 指定用哪个线程工厂创建线程: threadFactory,不能为null;
- 参数七
- 指定线程忙,任务满的时候,新任务来了怎么办: handler,不能为null;
- 临时线程触发机制
- 新任务提交时发现核心线程都被占用,任务队列也满了,但还可以创建临时线程,此时才会创建临时线程。
- 何时拒绝任务
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
“核心线程”如何理解呢?
如果把一个线程池,理解成一个公司,此时,公司里有俩类员工,一批是正式员工(有编制的),另一批是实习生(无编制的),正式员工的数目就是核心线程数,最大线程数就是正式员工+实习生。这个线程池里线程的数目是可以动态变化的,变化的范围就是[corePoolSize,maxmumPoolSize],正式员工可以摸鱼,不会因为摸鱼,被公司开除,但是实习生不允许摸鱼,如果这段时间任务多了,就可以多搞几个实习生,来干活,如果过段时间任务少了,并且少的状态持续了一段时间,空闲的实习生就被裁掉了。(这样做,既可以满足效率的要求,又可以避免过多的系统开销)
BlockingQueue<Runnable> workQueue 阻塞队列,用来存放线程池中的任务的,可以根据需要灵活设置这里的队列是啥,需要优先级,就可以设置PriorityBlockingQueue,如果不需要优先级,并且任务数目是相对恒定的,可以使用ArrayBlockingQueue,如果不需要优先级,并且任务数目变动较大,就用LinkedBlockingQueue。
ThreadFactory 工厂模式的体现,此处使用ThreadFactory作为工厂类,由这个类负责创建线程,使用工厂类创建线程,主要是为了再创建过程中,对线程的属性做出一些设置。(如果手动创建线程,就得手动设置在这些属性,就比较麻烦,使用工厂方法封装一下)
RejectedExecutionHandler 线程池的拒绝策略,一个线程池,能容纳的任务数量,是有上限的,当持续往线程池里添加任务的时候,一旦已经达到上限了,继续添加,会出现什么效果呢?(不同的拒绝策略,就有不同的效果)
🎈ThreadPoolExecutor的新任务拒绝策略
就比如一个学校老师,一个星期得上8节课,学校领导找到我,想让我去参加校园活动。
- 1.听到这个要求的时候,老师心态崩了,心情很烦躁——这属于(.AbortPolicy直接抛出异常)
- 2.老师直接和领导说,她这边有好多课去不了,让领导一个人去参加校园活动(.CallerRunsPolicy拒绝新任务,由新增任务的线程去执行)
- 3.老师给这一周8节课中一节课给割了,然后和领导一起去参加校园活动(.DiscardOldestPolicy丢弃任务队列中最老的任务,执行新任务去)
- 4.老师拒绝了校领导,继续去上课,然后校领导也不去了,这个校园活动都不去参加了。(DisCardPolicy丢弃新加的任务,新加任务的线程也丢弃了)
在面试中,拒绝策略和线程数目是面试的重点。
保持现状。