Java开发之线程池

虽然最近一年以来笔者的主要的开发内容都是前端开发方面的,Java相关的开发做的比较少,但是这些学过的知识也不能忘的一干二净,温故而知新,可以为师矣。下面来介绍一下笔者在2018年整理的关于Java线程池方面的知识。当然,这也是一个Java面试中常考的问题。

一. 池化技术

在介绍线程池之前首先需要了解的是池化技术(思想),这是一个开发领域中很常见的技术。

简单点来说,池化技术就是提前保存大量的资源,以备不时之需。 对于线程、内存、数据库的连接对象等等,这些都是资源, 程序中当你创建一个线程或者在堆上申请一块内存时, 都涉及到很多系统调用,也是非常消耗CPU的,如果你的程序需要很多类似的工作线程或者需要频繁的申请释放小块内存, 如果没有在这方面进行优化,那很有可能这部分代码将会成为影响你整个程序性能的瓶颈。 池化技术主要应用有线程池、内存池、连接池、对象池等等。

1.1 内存池

内存池 (Memory Pool)是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。 内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。

使用内存池的优点

1. 减少了内存碎片的产生。这个可以从创建内存池的过程中看出。我们在创建内存池时,分配的都是一块块比较整的内存块,这样可以减少内存碎片的产生。

 2. 提高了内存的使用效率。这个可以从分配内存和释放内存的过程中看出。每次的分配与释放并不是去调用系统提供的函数或是操作符去操作实际的内存,而是在复用内存池中的内存。

使用内存池的缺点

使用内存池很有可能会造成内存的浪费,原因也很明显,开始分配了一大块内存,不是全部都用得到的。

1.2 连接池

1. 数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。 一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样造成系统的性能低下。

2. 数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并将这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。 应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。

二. 线程池的概念

什么是线程池:java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

多线程技术主要解决处理器单元内多个线程执行的问题, 它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。假设一个服务器完成一项任务所需时间为:T1 创建线程时间, T2 在线程中执行任务的时间,T3 销毁线程时间。  如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

一个线程池包括以下四个基本组成部分:        

1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;        

2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;        

3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;      

 4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

使用线程池的好处:

1、提高资源利用率 线程池可以重复利用已经创建了的线程

2、提高响应速度 因为当线程池中的线程没有超过线程池的最大上限时,有的线程处于等待分配任务状态,当任务到来时,无需创建线程就能被执行。

3、具有可管理性 线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。

三. 线程池的常用API

1. 常见线程池:

① newSingleThreadExecutor 单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务。

②newFixedThreadExecutor(n) 固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行。 

③newCacheThreadExecutor 可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。

④newScheduleThreadExecutor 大小无限制的线程池,支持定时和周期性的执行线程。

2. 自建线程池:ThreadPoolExecutor构造方法

 3. ThreadPoolExecutor方法各参数意义

corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。 

threadFactory:线程工厂,主要用来创建线程;

handler:表示当拒绝处理任务时的策略,有以下四种取值:

4. 向线程池提交任务方法:

execute()方法: 实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

submit()方法: 是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。 

关闭线程池的方法:

showdown()方法: 不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务. showdownNow()方法: 立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务.

5. 其他API: getQueue() getPoolSize() getActiveCount() getCompletedTaskCount()……

四. 线程池相关实例

详见github: https://github.com/ZiTonzong/Java-ThreadPool

五. 合理配置线程池大小

高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换 ;

(2)并发不高、任务执行时间长的业务要区分开看: a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务 ; b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换 ;

(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

以上就是Java线程池的相关资料介绍及API方法的使用。线程池相关知识是Java面试中的一个重点知识,在特定的开发情景下也会遇到相关的问题,因此,了解其相关知识也是很有必要的。

参考:

https://blog.csdn.net/u010723709/article/details/50377543

https://www.cnblogs.com/dolphin0520/p/3932921.html

https://www.cnblogs.com/yulinfeng/p/7021293.html

https://www.cnblogs.com/aspirant/p/6920418.html

http://blog.sina.com.cn/s/blog_911f0b780102v3fk.html

发布了25 篇原创文章 · 获赞 21 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/a715167986/article/details/104030699