目录
🚩 int corePoolSize与int maximumPoolSize:
🚩 long keepAliveTime与TimeUnit nuit:
🚩 ThreadFactory threadFactory:
🚩 RejectedExecutionHandler handler:
📕 引言
之前呢我们对于并发编程,使用多线程就可以了,是因为线程比进程更轻量,在频繁创建和销毁的时候,更有优势,随着时代的发展,对于"频繁"二字有了新的认识。之前一个服务器 1s 处理 1k 个请求,就认为是频繁了。现在,一个服务器 1s 要处理几w的请求......
那如何优化呢? 我们就引入了线程池和协程(纤程)。本篇文章就来说说线程池,对于协程暂且不表,在Java8中还不支持,后序在高版本的Java中,引入的"虚拟线程"本质上就是协程。
为哈引入线程池或者协程就能够提升效率呢?
最关键的要点,直接创建/销毁线程,是需要在用户态+内核态配合完成的工作,对于线程池/协程,只需要在用户态即可,不需要内核态的配合。
那为什么说用户态+内核态配合完成就不够高效?
例子:
🌳 概念
线程池,是一种线程的使用模式,它为了降低线程使用中频繁的创建和销毁所带来的资源消耗与代价。
通过创建一定数量的线程,让他们时刻准备就绪等待新任务的到达,而任务执行结束之后再重新回来继续待命
想象这么一个场景:
假设有一个妹子,长得很好看并且又有才华,想要谈一个对象,如果妹子和这个对象谈腻歪了,就想要换一个,成本就比较高,效率就比较低,我需要先冷暴力他,消耗他的耐心,然后再提分手,分手完毕之后,还需要下一个小哥哥一点一点培养感情,这样效率确实很低。
为了提高效率,我就和在男朋友的交往过程中,提前和其他小哥哥搞暧昧,先把感情培养到位,这样,我只要和现男友分手,后面这个暧昧的小哥就可以直接转正。这样的小哥就称为"备胎",如果我需要频繁的更换男朋友,一个备胎不够用,就需要多搞几个,这就构成了"备胎池"。这样效率就提高了
所以字符串常量池,数据库连接池,线程池,进程池,内存池......思想都一样,用来提升效率。
线程池最核心的设计思路:复用线程,平摊线程的创建与销毁的开销代价
相比于来一个任务创建一个线程的方式,使用线程池的优势体现在如下几点:
- 避免了线程的重复创建与开销带来的资源消耗代价
- 提升了任务响应速度,任务来了直接选一个线程执行而无需等待线程的创建
- 线程的统一分配和管理,也方便统一的监控和调优
🍀ThreadPoolExecutor 类
标准库提供的线程池主要的类为:ThreadPoolExecutor
这个类使用起来比较复杂,构造方法很多,包含很多参数(面试考点,问这些参数是什么意思)。
在帮助手册java.util.concurrent(并发),这里面包含了很多多线程相关的工具或者是类
构造方法:这里面有四个构造方法,仔细观察发现里面的参数,越往下越全,所以我们只需要搞清楚最后一个,前三个也就清楚了。
这里面总共有7个参数,来说说这7个参数的意思:
🚩 int corePoolSize与int maximumPoolSize:
🚩 long keepAliveTime与TimeUnit nuit:
🚩 BlockingQueue<Runnable> workQueue:
🚩 ThreadFactory threadFactory:
🚩 RejectedExecutionHandler handler:
在使用线程池并且使用有界队列的时候,如果队列满了,任务添加到线程池的时候就会有问题,那么这些溢出的任务,ThreadPoolExecutor为我们提供了拒绝任务的处理方式,以便在必要的时候按照我们的策略来拒绝任务
线程池拒绝任务的时机有以下两种:
第一种情况是当我们调用 shutdown 等方法关闭线程池后,即便此时可能线程池内部依然有没执行完的任务正在执行,但是由于线程池已经关闭,此时如果再向线程池内提交任务,就会遭到拒绝。
第二种情况是线程池没有能力继续处理新提交的任务,也就是工作已经非常饱和的时候
标准库提供了四个解决方案:
线程池任务拒绝策略实现了 RejectedExecutionHandler 接口,JDK 中自带了四种任务拒绝策略。分别是AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy。其中AbortPolicy是ThreadPoolExecutor默认使用。
1,AbortPolicy(默认)
这种拒绝策略在拒绝任务时,会直接抛出一个类RejectedExecutionException的RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
2,DiscardPolicy
这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
3,DiscardOldestPolicy
如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
4,CallerRunsPolicy
相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处:
🎈第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
🎈第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
总结:
🎄标准库中的线程池
由于标准库自己也知道ThreadPoolExecutor使用起来比较费劲,于是标准库自己提供了几个工厂类,对于上述线程池又进一步封装了.
在标准库里面提供了一个 Executors类,这个类就是标准库提供线程池的工厂类
有几个不同的版本:主要使用前面两个
注意上述方法是有返回值的,返回值类型为 ExecutorService
代码一:我们可以看到是一个单独的线程,并非跟主线程是一个线程
代码二:
使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中
🏠 模拟实现线程池
接下来我们简单模拟实现一个简单的线程池
创建MyThreadPool实现我们的线程池
使用阻塞队列组织所有任务
构造方法里创建相应大小的线程数
提供一个submit方法使用线程池里面的线程
代码:
测试:
🙂 多线程初阶总结
🚩 保证线程安全的大致思路:
1,使用没有共享资源的模型
2,适用共享资源只读,不写的模型
不需要写共享资源的模型
使用不可变对象
3,直面线程安全(重点)
保证原子性
保证顺序性
保证可见性
🚩对比线程和进程
📌线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
📌进程与线程的区别
进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
线程的创建、切换及终止效率更高