JavaSE学习笔记 走进Java线程池,解决你对Java线程池的种种疑问 (二)

Executors是一个Java中的工具类,提供工厂方法来创建不同类型的线程池。
在这里插入图片描述

前面我们已经介绍了关于Executors工具类中常用的创建线程池的方法,我们简单进行回顾下:

  • 1.newSingleThreadExecutor
newSingleThreadExecutor
介绍:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来代替它。此线程池保证所有的任务的执行顺序按照任务的提交顺序执行。
优点:单线程的线程池,保证线程的顺序执行
缺点:不适合并发
  • 2.newFixedThreadPool
newFixedThreadPool
介绍:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大值。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
优点:固定大小线程池,超出的线程会在队列中等待
缺点:不支持自定义拒绝策略,大小拒绝,难以扩展
  • 3.newCachedThreadPool
newCachedThreadPool
介绍:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲的线程。当任务增加时,此线程又可以添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者JVM)能够创建的最大线程大小。
优点:很灵活,具有弹性的线程池线程管理,用多少线程池给多大的线程池,不用后及时进行回收,用时可以进行新建
缺点:一旦线程无限增长,会导致内存溢出
  • newScheduledThreadPool
newScheduledThreadPool
介绍:创建一个定长线程池,支持定时与周期性任务的执行
优点:一个固定大小的线程池,可以定时或者周期性执行任务
缺点:任务是单线程方式执行,一旦一个任务失败其他任务也会受到影响

注意:

1.以上线程池都不支持自定义拒绝策略
2.newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至会导致内存溢出

3.newCachedThreadPool和newScheduledPool:
主要问题是线程最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至内存溢出

由于Executors中提供创建线程池的方法具有以上的弊端,所以会推荐使用ThreadPoolExecutor创建线程池,集以上优点于一身。


1.对ThreadPoolExecutor的理解

Java提供了实现的线程池的模型–ThreadPoolExecutor,可以这样理解Java线程池:就是为了最大化高并发带来的性能提升,并最小化手动创建线程的风险,将多个线程统一在一起管理的思想。

  • 首先,我们需要关注ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

细看ThreadPoolExecutor的构造方法还是比较复杂的,下面我们探讨下构造方法中的核心参数。


1.1 ThreadPoolExecutor核心参数理解(面试常问)

参数名称参数解释
corePoolSize表示常驻核心线程数,如果大于0,即使本地任务执行完也不会被销毁。
maximumPoolSize表示线程池能够容纳可同时执行的最大线程数
keepAliveTime表示线程池中线程空闲的时间,当空闲时间达到该值时,线程会被销毁,只剩下corePoolSize个线程位置
unitkeepAliveTime的时间单位,最终都会转换为纳秒
workQueue当请求线程数大于maximumPoolSize时,线程进入该阻塞队列
threadFactory表示线程工厂,用来生产一组相同任务的线程,同时也可以通过增加前缀名,虚拟机栈分析时会更加清晰
handler执行拒绝策略,当workQueue达到上限,同时也达到maximumPoolSize就要通handler来进行处理。例如拒绝,丢弃等,这是一种限流的保护措施

整体任务执行如下:

试想,如果有请求就创建一个线程,请求结束就销毁一个线程,频繁往复这样操作,这样的代价是不能接受的。

可以看到,使用线程池不但完成手动创建线程可以做到的工作,同时也填补了手动线程不能做到的空白。

由此可见线程池的作用可以包含:

1.实现任务线程队列缓存策略和拒绝机制
2.利用线程池管理线程,控制最大并发数(手动创建线程很难得到保证)
3.可以实现定时执行,周期执行等功能


1.2 线程池中任务的执行过程分析

当任务提交到线程池中需要经过以下过程:

在这里插入图片描述

执行流程解析
1.首先检查核心线程池是否已满。这个核心线程池,就是不管用户量多少,线程池始终维持的线程的池子。在这里可以假如说线程池的总容量装100个线程,核心线程数我们设置为50,那么无论用户量有多少,都保持50个线程存活着。核心线程池中线程数量是根据具体业务需求来决定的。
2.判断阻塞队列是否已满,阻塞队列有很多种,在下面也会简单介绍各种阻塞队列
3.最后判断线程池是否已满。按照前面的例子,就是判断线程数是不是100个线程,而不是50个。
4.如果满了,就不能继续创建线程了,就需要按照饱和策略或者拒绝策略来进行处理。拒绝策略在下面也会简单介绍。

简单来说:有请求时,创建线程执行任务,当线程数量等于corePoolSize(常驻核心线程数),请求假如阻塞队列里,当队列满了时,接着创建线程,线程数等于maximumPoolSize。当任务处理不过来的时候,线程池开始执行拒绝策略。


1.3 阻塞队列的介绍

  • 何为阻塞?简单的来说,食堂打饭的窗口只有一个,下课了同时来了很多同学,窗口每次只能服务于一个学生。剩下的学生只能排队等待。

  • 何为队列?先进先出的一种数据结构(容器),例如:食堂排队打饭。

  • 什么时候使用阻塞队列?生产者消费者模式

阻塞队列的工作流程:

在这里插入图片描述

  • 下面是阻塞队列的介绍:
阻塞队列的名称含义
ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列
LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列,默认大小为Integer.MAX_VALUE
PriorityBlockingQueue一个支持优先级排序的无界阻塞队列
DelayQueue一个使用优先级队列实现的无界阻塞队列}
SynchronousQueue一个不存储元素的阻塞队列。如果生产者线程将元素放入队列,消费者线程没有从队列中拿走元素,那么该队列会进入阻塞状态
LinkedTransferQueue一个由链表结构组成的无界阻塞队列
LinkedBlockingDueue一个由链表结构组成的双向阻塞队列

1.4 线程池拒绝策略

我们很难准确的预测未来的最大并发量,所以定制合理的拒绝策略是不可忽略的步骤。ThreadPoolExecutor提供了四种拒绝策略:

在这里插入图片描述

  • 1.AbortPolicy:默认的拒绝策略,会throw RejectedExecutionException拒绝
  • 2.CallerRunsPolicy:提交任务的线程自己去执行该任务
  • 3.DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列
  • 4.DiscardPolicy:相当大胆的策略,直接丢弃任务,没有任何异常抛出

不同的框架也都有不同的拒绝策略,我们也可以通过实现RejectedExecutionHandler 自定义的拒绝策略。

没有绝对的拒绝策略,只有适合业务需求的拒绝策略,但在设计过程中千万不要忽略掉拒绝策略就可以。


总结

当我们需要频繁的创建线程时,我们要考虑到利用线程池统一管理线程资源,避免额外的开销和不可控的风险。同时了解到了线程池的几个核心参数之后,我们需要经过调优的过程来设置最佳线程参考值。希望大家看到此文对大家线程池部分的学习有所帮助。
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值