多线程编程设计模式(单例,阻塞队列,定时器,线程池)(三)+https://developer.aliyun.com/article/1413588
3.java标准库内部的线程池
1.基本概念
java标准库内部其实实现了线程池,线程池被封装成了一个类ThreadPoolExecutor
创建出一个线程池
// 向上转型 ExecutorService service = Executors.newCachedThreadPool();
\ \ 此处线程池的创建并没有通过常规的new关键字创建,而是调用了Executors内部的一个方法来创建线程池对象,这种创建对象的方式我们称之为工厂模式,工厂模式也是设计模式的一种,工厂模式的存在主要是为了解决构造方法缺陷,有时候我们对一个类的实现希望其有多种方式,而实现需要通过构造方法来创建,由于构造方法的方法名只能是类名,这就带来了一些使用上的局限性,请看下图
同理,对于ThreadPoolExecutor的创建来说,他也有很多种实现方式
,为了更好的调用,此处就采用工厂模式进行创建(可以将对象的创建方法总结为以下两种)
2.ThreadPoolExecutor的实现方式
ThreadPoolExecutor一般有四种实现方式:
前两种实现方式比较常用,后两种实现方式不常用,了解即可
3.ThreadPoolExecutor的核心方法
核心方法就两个:
- 构造方法
- 任务提交方法
1.任务提交
任务提交方法比较简单,创建好ThreadPoolExecutor对象之后使用submit方法进行任务的提交,交给线程池内部线程去执行提交的任务
service.submit(new Runnable() { @Override public void run() { System.out.println("任务提交"); } });
更重要的是ThreadPoolExecutor的构造方法,这也是面试常考的!!!
2.构造方法(重点)
进入到javase的标准文档,查看ThreadPoolExecutor的构造方法
一共有四个构造方法,但实际上前三种都是简化版本,省去了一些参数,第四种是最全的构造方法,这里重点掌握第四种方法
依次来看第四种构造方法的参数
**int corePoolSize, int maximumPoolSize**
corePoolSize:核心线程的数目
maximumPoolSize:线程池内部最多持有的线程数目
什么是核心线程呢?对于一个线程池来说,其内部存储的线程分为两类:
- 核心线程
- 非核心线程
核心线程是一个线程池内部始终持有的线程,无论任务有多少,核心线程的数目始终固定不变;非核心线程不是线程池始终持有的,可以根据要执行任务的多少添加或删除,当任务多时,就新建几个非核心线程去应对高任务量,任务少时就删除几个非核心线程.
可以把核心线程想象为一个公司的正式员工,而非核心线程就是实习生,对于正式员工来说,是不能随便删除的(因为劳动法~),而实习生是可以随便开除的,当任务多时,就多招几个实习生来帮我干活,任务少了,就开除这些实习生(老铁扎心了吧
)
核心线程保证了低负载情况下任务的正常运行,非核心线程可以有效应对高负载的情况
**long keepAliveTime, TimeUnit unit**
keepAliveTime:非核心线程在空闲状态下的存活时间
unit:时间单位
对于非核心线程来说,如果在一定时间内处于空闲状态,没有执行任务,系统就会讲这些空闲的非核心线程销毁,节省系统资源,keepAliveTime就是规定最多空闲时间是多少,TimeUnit unit是空闲时间的单位,TimeUnit 是一个枚举类型,里面存放时间的的单位(秒/分/时)
比如:keepAliveTime是5,unit为TimeUnit.SECONDS,这意味着非核心线程在空闲5s之后就会被销毁
**BlockingQueue<Runnable> workQueue**
workQueue:用于存放要执行任务的阻塞队列,等待线程池中的线程从阻塞队列中获取相应的任务并执行,此时用户端就是生产者,执行任务的线程池就是消费者.队列中的元素就是要执行的任务Runnable
不同的阻塞队列的使用场景也不同,主要考虑容量限制和阻塞策略,可以根据不同队列的性质进行选择
**ThreadFactory threadFactory**
threadFactory:通过工厂模式创建出来的定制化的线程
ThreadFactory 是一个接口,只有一个方法,用于创建自定义的线程
public interface ThreadFactory { Thread newThread(Runnable r); }
也就是ThreadFactory threadFactory这个参数就是让我们为线程池提供自己创建的自定义的线程,以下是一个简单的使用例子
// 创建自定义的线程 先让其实现ThreadFactory接口 class MyCustomThread implements ThreadFactory { // 设置属性 自定义线程的前缀 private final String threadNamePrefix = "MyCustomThread - "; // 自定义线程的编号 private int threadCnt = 0; @Override public Thread newThread(Runnable r) { // 规定线程要执行的任务 Thread t = new Thread(r); t.setName(threadNamePrefix + ++threadCnt); return t; } } public class Demo2 { public static void main(String[] args) { MyCustomThread myCustomThread = new MyCustomThread(); // 利用自定义线程创建出线程池 ExecutorService executorService = Executors.newFixedThreadPool(5,myCustomThread); // 提交任务 for (int i = 0; i < 5; i++) { executorService.submit(new Runnable() { @Override public void run() { // 打印当前正在执行任务的线程名称 System.out.println(Thread.currentThread().getName()); } }); } } }
打印结果:
注意:因为在多线程编程中,线程的调度是随机的,所以每次打印的结果也是不同的
**RejectedExecutionHandler handler**
handler:线程池的拒绝策略 对于一个线程池来说,其能容纳的线程数量是有限的,当超过最大的容量时,线程池会有一定的拒绝策略来阻止容量超过最大的限制,不同的拒绝策略有不同的效果,具体来说有以下四种拒绝策略:
- 直接抛出异常(我就是不让你超过限制,一超过限制就报错)这种策略是默认策略
- 丢弃当前新加的任务(添加进来新的任务就抛弃)
- 丢弃任务队列中最老的任务(老弱病残终究会被淘汰的)
- 添加的任务由添加任务的线程负责执行(这样做是为了尽量不丢失任务,添加任务的线程不是线程池中的线程,哪个线程往线程池中提交的任务就交给谁执行)
以上就是ThreadPoolExecutor类构造方法所有参数的讲解,其中corePoolSize和RejectedExecutionHandler是面试中最常考的!!!
4.线程池的模拟实现
如何去模拟实现一个线程池呢?需要先清楚了解线程池的基本组成,线程池由以下三部分组成
- BlockingQueue:任务队列 用于存放线程池中线程要执行的任务
- 线程池:线程池的核心主体还是多个用于执行任务的线程
- submit方法:用于提交任务,用于连接任务队列和线程池
代码实现:
class MyThreadPool { // 创建一个任务队列 用于存放线程池要执行的任务 10代表次任务队列最多存放的任务数量是10 超过10就要阻塞等待 private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10); // 创建提交方法 将任务提交到任务队列之中 public void submit(Runnable runnable) throws InterruptedException { // 此处采用的拒绝策略就是使用阻塞队列 队列满 阻塞等待 queue.put(runnable); } // 创建构造方法 public MyThreadPool(int n) { // 创建n个线程 就相当于newFixedPool的效果 创建出制定容量的线程池 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 Demo3 { public static void main(String[] args) throws InterruptedException { MyThreadPool myThreadPool = new MyThreadPool(10); for (int i = 0; i < 10; i++) { final int id = i; myThreadPool.submit(new Runnable() { @Override public void run() { System.out.println("线程执行id" + id); } }); } } }
运行结果:
5.线程数量如何决定?
在使用线程池的时候,如何确定线程池内部的线程数量呢?在网上其实有很多种说法,假设cpu的逻辑核心数是N,线程的数目可以设置为N,N-1,N-2,2N,1.5N等等,其实这些说法都不准确,再你没有接触到实际的项目之前,线程的数目是不可能确定的
我们要执行的代码可以分为以下两类:
- cpu密集型:代码中大量存在需要进行算术运算和逻辑判断
- I/O密集型:代码涉及到I/O操作
假设一个代码中都是cpu密集型的代码(很吃cpu资源),cpu的逻辑核心数是N,那你设置的线程数目最多只能是N,一个线程的执行对应着一个cpu逻辑核心,如果创建的线程数目比逻辑核心N还多,就没有cpu来执行多出的线程了,反而造成了资源的浪费
假设一个代码中全是I/O密集型的代码,此时线程池中的线程数目是可以大于N的,因为一个cpu上可以调度执行这些操作~I/O操作不吃cpu
对于我们所写的代码来说,我们不知道有多少cpu密集型的,有多少I/O密集型的,也就无法直接确定设置的线程数目,正确方法应该是通过性能测试来确定线程的数量,即不断地更换线程数,看什么情况下能达到性能的最优化,通过测试找出应设置的线程数
补充:I/O操作是指计算机系统与外部链接设备之间的数据传输,常见的I/O操作包括文件读取,数据库连接,网络通信等等
6.线程池构造方法的进一步认识
1.newFixedThreadPool
源码:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
讲解
2.newCachedThreadPool
源码:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
讲解:
3.newSingleThreadExecutor
源码:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
讲解:
4.ScheduledThreadPoolExecutor
源码:
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
讲解:
最后附上多线程设计模式的思维导图
创作不易!!!欢迎大家多多转发支持!