这里写目录标题
由浅入深理解线程池
第一章:创建线程的几种方式
继承Thread类
缺点:OOP单继承的局限性
class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("Mythread1");
}
}
实现 Runnable 接口
class Mythread2 implements Runnable{
@Override
public void run() {
System.out.println("Mythread2");
}
}
解决了OOP单继承的局限性
避免了OOP单继承的局限性,灵活方便,方便同一个对象被多个线程使用
实现Callable接口
Runnable接口中run方法没有返回值,并且不可以抛出异常,使用Callable接口中的call方法可以有返回值,并且可以抛出异常。
class Mythread3 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"启动了");
return 1024;
}
}
上面三种方式的测试
public class CreateThread {
public static void main(String[] args) throws InterruptedException, ExecutionException {
new MyThread1().start();
new Thread(new Mythread2()).start();
FutureTask<Integer> futureTask=new FutureTask<>(new Mythread3());
Thread t1=new Thread(futureTask);
t1.start();
int result01=100;
int result02=futureTask.get();//建议放到最后,要求获得Callable线程的计算结果,如果没有计算完成,就去强求,会导致阻塞直到计算完成
System.out.println("****result*****"+(result01+result02));
}
}
使用线程池
1、什么是线程池?
线程池: 简单理解,它就是一个管理线程的池子。
2、为什么要使用线程池
- 它帮我们管理线程(解耦),避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
- 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
- 重复利用。线程用完,再放回池子,可以达到重复利用的效果,节省资源。
3、怎么用?
创建线程池的API
- Executors.newCachedThreadPool():无限线程池(一池多线程)——执行短期异步的小程序或者负载较轻的服务器。
- Executors.newFixedThreadPool(nThreads):创建固定大小的线程池(一池固定数目线程)——适用场景:执行长期任务,性能好很多。
- Executors.newSingleThreadExecutor():创建单个线程的线程池(一池一线程)——适用于一个任务一个任务执行的场景。
public class MythreadPoolDemo {
public static void main(String[] args){
//ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池5个处理线程
ExecutorService threadPool = Executors.newFixedThreadPool(1);//一池1个线程
// ExecutorService threadPool = Executors.newCachedThreadPool();//一池N个线程
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
try{
for(int i=1;i<=10;i++){
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
注意: 在实际的开发过程中,以上三个方法都不使用,而是直接通过new ThreadPoolExecutor()来创建。因为以上三种方法都会导致OOM,这一点在第三章会具体介绍。
public static void main(String[] args){
ExecutorService threadPool = new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
try{
for(int i=1;i<=11;i++){
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
// threadPoolInit();
}
第二章:线程池底层剖析
Excutors提供的三种创建线程池的方法底层都调用了ThreadPoolExecutor()方法:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
ThreadPoolExecutor的API如下
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
下面我们来剖析每个参数的含义:
- corePoolSize: 线程池核心线程数最大值
- maximumPoolSize: 线程池最大线程数大小
- keepAliveTime: 线程池中非核心线程空闲的存活时间大小
- unit: 线程空闲存活时间单位
- workQueue: 存放任务的阻塞队列
- threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
- handler: 线城池的饱和策略事件,主要有四种类型。
对比Executors提供的三种方法底层参数的值我们可以发现,newCachedThreadPool,底层corePoolSize为0,maximumPoolsize为Integer.MAX_VALUE ,表示初始时线程池中没有线程,然后会根据需要不断增加线程数,线程池最大线程数为Integer.MAX_VALUE,这会导致OOM。线程存活时间keepAlivaTime为60L,表示线程超过这个时间之后,非核心线程会被销毁,知道减至核心线程数。存放任务的阻塞队列为SynchronousQueue,关于阻塞队列的知识,后期我们会进行详细的总结。
类比newCachedThreadPool我们就可以理解newFixedThreadPool和newSingleThreadExecutor的参数的含义了,它们核心线程数和最大线程数均为固定的一个值,线程池创建好之后就创建这些线程,并等待被使用。
下面贴出美团技术博客中的图,大佬就是大佬,描述的清晰明了:
第三章:正确的使用线程池
使用Executors创建线程池会带来的问题
我们首先来看看阿里开发手册中的规定:
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
这两个方法底层使用的是无界队列LinkedBlockingQueue,堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
除此之外拒绝策略 hanlder设置不当也会导致任务提交时出现异常。
如何正确的使用线程池
对于如何正确的使用Java线程池,这里参考了Java线程池详解,主要分为以下几部分
避免使用无界队列
不要使用 Executors.newXXXThreadPool() 快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
new ThreadPoolExecutor.DiscardPolicy());
明确拒绝任务时的行为
任务队列总有占满的时候,这时再 submit() 提交新的任务会怎么样呢? RejectedExecutionHandler 接口为我们提供了控制方式,接口定义如下:
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
获取处理结果和异常
如何设置线程池的参数
CPU密集型与IO密集型
CPU密集的意思是该任务需要大量的运算(计算密集型),而没有阻塞,CPU一直全速运行。CPU密集型任务配置尽可能少的线程数量:一般公式为:CPU核数+1个线程的线程池。
需要注意的是CPU密集任务只有在真正的多核CPU上才能全速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的计算能力就这些了。
由于IO密集型任务线程并不是一直执行任务,则应配置尽可能多的线程,如CPU核数*2
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间。
动态设置参数
动态设置参数这块儿,暂时还没有深究,需要写完demo在回来完善,有兴趣可以先看看文末两个参考文献。
线程池在项目中的应用
在美团的技术文章中提到了两个应用场景:
场景一:快速响应用户请求
描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。。
分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
场景二:快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。
分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
参考资料:
Java线程池实现原理及其在美团业务中的实践
如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。
Bilibili——周阳JUC视频