📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍
文章目录
- 线程池运用背景
- 线程池技术简介
- 线程池核心参数
- Executors 四种线程池
- 线程池知识补充
线程池运用背景
|- 为什么用?
创建一个新的线程可以通过继承Thread类或者实现Runnable接口来实现,这两种方式创建的线程在运行结束后会被虚拟机销毁,进行垃圾回收,如果线程数量过多,频繁的创建和销毁线程会浪费资源,降低效率。而线程池的引入就很好解决了上述问题,线程池可以更好的创建、维护、管理线程的生命周期,做到复用,提高资源的使用效率,也避免了开发人员滥用new关键字创建线程的不规范行为。
说明:阿里开发手册中明确指出,在实际生产中,线程资源必须通过线程池提供,不允许在应用中显式的创建线程。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
|- 传统的线程使用
需要使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间(即线程创建和销毁的时间都超过了任务本身的执行时间,那还不如直接同步执行任务)。若线程数超过一定数量,还可能导致有的线程无法得到执行,甚至程序挂掉。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
Tips:和上面一个
|- 线程池的作用
线程池就是用来管理线程的工具。
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
在开发过程中,合理地使用线程池能够带来3个好处:
第一:降低资源消耗,通过重复利用已创建的线程,降低在创建和销毁线程上所花的时间以及系统资源的开销;
第二:提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行;
第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。可以根据系统的承受能力,调整线程池中工作线线程的数目,使用线程池可以进行统一分配、调优和监控(默认情况下,一个线程的栈要预留1M的内存空间)。
总结:一句话就是线程池可以更合理的利用线程,控制线程数量,提升系统稳定新。
|- 什么场合要使用线程池
1、单个任务处理的时间比较短 ;
2、将需处理的任务的数量大 ;
说明:即常说的高并发低耗时的情况,像长时间开着的某个任务就不适合线程池。
通俗来说,线程池,就是在调用线程的时候初使化一定数量的线程,有线程过来的时候,先检测初使化的线程还有空的没有,没有就再看当前运行中的线程数是不是已经达到了最大数,如果没有,就新分配一个线程去处理,就像餐馆中吃饭一样,从里面叫一个服务员出来;但如果已经达到了最大数,就相当于服务员已经用于了,那没得办法,另外的线程就只有等了,直到有新的“服务员”为止。线程池的优点就是可以管理线程,有一个高度中枢,这样程序才不会乱,保证系统不会因为大量的并发而因为资源不足挂掉。
线程池技术简介
|- JDK1.5与线程池
线程的使用在Java中占有极其重要的地位,在JDK1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在JDK1.5之后这一情况有了很大的改观。JDK1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。
|- 线程池涉及的类
1、Executor 是一个顶层接口,在它里面只声明了一个方法 execute(Runnable),返回值为void,参数为Runnable类型,代表用来执行传进去的任务,严格意义上讲,Executor并不是一个线程池,而只是一个执行线程的工具;
package java.util.concurrent;public interface Executor {void execute(Runnable var1);
}
2、ExecutorService 继承并扩展了Executor接口,提供了 submit、shutdown等方法扩展;
3、AbstractExecutorService 抽象类实现了 ExecutorService 接口,基本实现了 ExecutorService 中声明的所有方法;
4、ThreadPoolExecutor 是线程池的核心实现类,继承了类 AbstractExecutorService,在 ThreadPoolExecutor 类中有几个非常重要的方法,比如 execute,实际上是Executor中声明的方法,在 ThreadPoolExecutor 进行了具体的实现,通过这个方法可以向线程池提交一个任务,交由线程池去执行;
5、ScheduledExecutorService继承ExecutorService接口,并定义延迟或定期执行的方法;
6、ScheduledThreadPoolExecutor继承ThreadPoolExecutor并实现了ScheduledExecutorService接口,是延时执行类任务的主要实现;
Tips:ThreadPoolExecutor 是线程池的核心实现类,最常用。
|- 线程池运行原理
线程池核心参数
背景说明
在JDK帮助文档中,有如此一段话:
“强烈建议程序员使用较为方便的 Executors 工厂方法 Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池) Executors.newSingleThreadExecutor()(单个后台线程)它们均为大多数使用场景预定义了设置。”
但是,在观看阿里巴巴开发规约的时候,建议程序员使用精确的属性去构造,如下:
(强制)线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 创建线程池方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
说明:Executors的newFixedThreadPool等方法,实际上也是一些默认参数的ThreadPoolExecutor构造,之所以要直接使用ThreadPoolExecutor还是想让使用者更了解这些参数,选择更合适的方式。
ThreadPoolExecutor 构造方法
Executors 接口提供的四种创建线程池的方法,底层都是调用ThreadPoolExecutor的构造方法。
ThreadPoolExecutor 类中提供了四个构造方法,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。
第四个构造方法的属性也是最全的,方法体代码如下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
corePoolSize(基本线程数)
核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。
默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
线程池中最核心的线程池数据量,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize(最大线程数)
线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
线程池中最大的线程数量,如果线程池中的线程数据小于最大数,并且提交新的任务,此时会创建新的线程来执行,直到达到最大线程数,再提交新的任务,此时会执行到对应的队列里面,后面的参数会讲到,队列满了在执行相应的策略,后面的参数会讲到。
keepAliveTime(超过基本线程数的空闲线程多久关闭)
1、表示线程没有任务执行时最多保持多久时间会终止。
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
2、线程活动保持时间,线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
3、当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。说的让人感觉比较模糊,总结一下大概意思为:比如说线程池中最大的线程数为50,而其中只有40个线程任务在跑,相当于有10个空闲线程,这10个空闲线程不能让他一直在开着,因为线程的存在也会特别好资源的,所有就需要设置一个这个空闲线程的存活时间,这么解释应该就很清楚了。
unit(上面那个属性的单位)
参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:TimeUnit.DAYS 等;
线程活动保持时间的单位,该时间是针对keepAliveTime的时间单位,可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
workQueue:
一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
任务队列,用于保存等待执行的任务的阻塞队列。后面再详细的讲解各个队列的特点。
threadFactory(执行程序创建新线程时使用的工厂):
线程工厂,主要用来创建线程,用于新提交的任务,并且线程池中没有空闲的线程,并且线程池的大小没有达到最大线程池数据量时需要创建的线程,由该线程工程类创建线程。
handler(由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序)
表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
执行策略,执行策略是在一定的前提下执行的,当线程池的线程数量已经达到最大值,并且队列已经达到最大值时,对新提交的线程任务进行的反应策略,后面在详细的讲解执行策略的问题。
Executors 四种线程池
|- 前言
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。|- newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行(先进先出)。
底层实现:new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
底层分析:核心线程数和最大线程数都是1,空闲线程保持时间为0(意思就是不缓存),队列采用LinkedBlockingQueue。|- newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
底层实现:new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
底层分析:核心线程数和最大线程数一样,空闲线程保持时间为0(意思就是不缓存),队列采用LinkedBlockingQueue。
说明:定长线程池的大小最好根据系统资源进行设置,如 Runtime.getRuntime().availableProcessors(),几核就代表需要几个线程;|- newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
底层实现:new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
底层分析:可以看到核心线程数为0,最大线程数无限大,回收空闲线程时间60秒,缓存队列使用SynchronousQueue(只允许放一个即被阻塞),这种线程池有任务来就会一直创建新的线程,对性能损耗较大,但是处理效率最高。
说明:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
说明:不太适合非常频繁的场景,等下服务器挂了。|- newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求(schedule方法,延迟N秒执行)。
底层实现:super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
线程池知识补充
|- 阻塞队列和非阻塞队列阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列。ConcurrentLinkedDeque 是典型的无界非阻塞队列,是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素(简单了解即可)。BlockingQueue 是典型的阻塞队列,在Java中,BlockingQueue的接口位于java.util.concurrent 包中(在Java5版本开始提供),由上面介绍的阻塞队列的特性可知,阻塞队列是线程安全的。在线程池中,底层使用阻塞队列BlockingQueue实现。|- 常见阻塞队列(缓存队列)
SynchronousQueue:队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。
LinkedBlockingQueue:阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据。
补充:Integer.MAX_VALUE最大值,值为 2的31次方-1 的常量,它表示 int 类型能够表示的最大值 214748364,基本无限大。|- 任务和线程的区别
线程可以理解为用来执行任务的程序,任务就是你要做的事情,大多时候不用去区分。
并发队列里面存放的是线程而不是任务,可以看到线程池的execute方法的作用就是提交线程任务。
其参数就是一个Runnable接口,也就是线程,意思就是提交一个线程任务,任务内容就是run方法里面的。