多线程之线程池
多线程作为目前项目中使用频率非常高的技术,为了在以后的工作中能够得心应手,随心所欲的使用,我们有必要对他进行深入了解,在了解线程池之前,我们需要对多线程有一定的认识。
多线程概念
进程和线程
进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。是资源分配的基本单位。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。是资源调度的基本单位。
进程与线程之间的联系和区别:一个程序运行至少有一个进程,一个进程中可以包含多个线程。线程是进程的组成部分,一个线程必然拥有一个父进程。
单线程和多线程
单线程:多个任务只能顺序执行,下一个任务或功能的执行必须是在上一个任务或功能执行完毕之后。这样的执行会有等待执行的过程,当大量任务需要执行时,系统只有一个线程顺序执行,会造成系统资源的浪费以及任务执行时间过长,在实际项目中可能长时间未响应,影响程序的正常运行和功能的正常使用
多线程:多个任务可以由多个线程并发的执行,缩短任务完成需要的时间,同时充分利用系统CPU资源。但这个过程中的顺序难以保证,需要我们用一定的技术手段去实现可控的顺序以及执行结果。
多线程的产生条件
多线程产生的条件是竞态条件和临界区,二者缺一不可。
竞态条件:多个线程访问的是相同的资源,同时更新共享资源,对访问熟悉怒敏感,会形成一个竞争关系
临界区:导致静态条件发生的代码区
多线程的意义
- 进程之间不能共享内存,但线程之间可以,且十分容易
- 系统创建进程需要为该进程重新分配资源,但创建线程开销相对较小,因此对多线程来实现多任务比多进程的效率更高
- 充分利用系统CPU资源,减少系统CPU空闲导致资源浪费的时间
- 提高计算机的执行效率。 在计算机中提高程序的执行效率有三种方法:一、是增加计算机的CPU个数。二、是为一个程序启动多个进程。三、是在程序中使用多线程。因为CPU的昂贵,所以第一种不好。又因为每一个进程都是在独立的内存空间中运行,并且内存不共享,数据不共享,所以启动多进程会消耗大量系统资源,第二种不好。第三种继承了他们的优点,弥补了他们的缺点,并且多线程可以模拟多块CPU的运行方式,因此多线程是提高程序执行效率的最廉价方式
- 缩短任务完成所花费的时间,缩短程序的响应时间
- 简化程序设计。有一个例子说明,通过手工编程的方式在一个单线程应用程序中按照上面的顺序读取和处理文件,你还必须得跟踪每个文件的读取和处理状态。你可以启动两个线程来代替这种手工编程的方式,每个线程负责读取和处理一个文件。每个线程在等待磁盘读取自己的文件的时候,都会被阻塞。一个线程在等待磁盘读取文件的同时如果另外一个线程的文件已经读取完毕了,这时候另外一个线程可以利用 CPU 处理部分文件。结果就是,磁盘一直处于忙碌状态,把各种文件都读取到内存中。这就提高了磁盘和 CPU 的利用率。由于每个线程只跟踪一个文件,这种情况编程也更容易实现。
多线程引发的思考—线程是不是越多越好?
答案是否定的。因为以下这些原因
- 线程是java的一个对象,更是操作系统的资源,线程创建、销毁需要时间。如果创建时间+销毁时间>执行任务时间则很不合算
- java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认的最大栈大小是1M,这个栈空间需要从系统内存中分配。线程过多,会消耗很多内存
- 操作系统需要频繁切换上下文,影响性能
由此,我们引入了线程池,用来控制多线程的数量
线程池概念
线程池技术关注如何缩短线程的创建时间和销毁时间,实现线程复用,从而提高程序性能
线程池的组成
一个线程池由组成部分:线程池管理器、工作线程、任务接口、任务队列
- 线程池管理器:用于创建并管理线程池,包括创建线程池、销毁线程池、添加新任务
- 工作线程:线程池中的线程,在没有任务时处于等待状态,可以循环执行任务
- 任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等
- 任务队列:用于存放没有处理的任务,提供一种缓存机制
线程池常用API说明
- Executor
最上层接口,定义了 执 行 任 务 方 法 e x e c u t e \color{red}{执行任务方法execute} 执行任务方法execute - ExecutorService
继承了Executor接口,拓展了Callable、Future、关闭方法 - ScheduledExcutorService
继承了ExecutorService,增加了定时任务相关方法 - ThreadPoolExecutor
基 础 、 标 准 的 线 程 池 实 现 \color{red}{基础、标准的线程池实现} 基础、标准的线程池实现 - ScheduledThreadPoolExecutor
继承了ThreadPoolExecutor,实现了Schedule的ExectorService中相关 定 时 任 务 \color{red}{定时任务} 定时任务的方法 - Executors工具类
- newFixedThreadPool(int nThreads)
创建一个固定大小、任务队列容量无界的线程池。核心线程数=最大线程数 - newCachedThreadPool()
创建一个大小无界的缓冲线程池。它的任务队列是一个同步队列。任务加入到池中,如果池中有空闲线程,则交由空闲线程执行;若无则创建新的线程执行。缓冲池中的空闲线程空闲时间超过60秒,则被销毁释放。线程数随着任务的多少而变化。适用于执行耗时较小的任务。池的核心线程数=0,最大线程数=Integer.MAX_VALUE - newSingleThreadExecutor()
只有一个线程来执行无界任务队列的单一线程池。该线程确保任务按加入的顺序依次执行。当唯一的线程因任务异常中止时,将创建一个新的线程来继续执行后续任务。与newFixedThreadPool(1)的区别在于,单一线程池池的大小在newSingleThreadExecutor方法中硬编码,不定为1,,不能再改变 - newScheduledThreadPool(int corePoolSize)
能定时执行任务的线程池。该池的核心由参数指定,最大线程数=Integer.MAX_VALUE
- newFixedThreadPool(int nThreads)
在以上的api中,我们可以看见ThreadPoolExecutor是线程池中最核心的类。我们了解下这个类
public class ThreadPoolExecutor extends AbstractExecutorService {
.....
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
...
}
从上面的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。
下面解释下一下构造器中各个参数的含义:
- corePoolSize
核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中; - maximumPoolSize
线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程; - keepAliveTime
表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize - keepAliveTime
如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0; - unit
参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS //天
TimeUnit.HOURS //小时
TimeUnit.MINUTES //分钟
TimeUnit.SECONDS //秒
TimeUnit.MILLISECONDS //毫秒
TimeUnit.MICROSECONDS //微妙
TimeUnit.NANOSECONDS - workqueue
一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。 - threadFactory
线程工厂,主要用来创建线程; - handler
表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy 也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy 由调用线程处理该任务
线程池的运行原理
任务execute过程
- 是否达到核心线程数量?没达到,创建一个工作线程来执行任务
- 工作队列已满?没满,则将新提交的任务存储到工作队列中
- 是否达到线程池最大数量?没达到,则创建一个新的工作线程来执行任务
- 最后,执行拒绝策略来处理这个任务
流程运转如下图所示:
线程池的使用
1.明确标准线程池中的core,max,queue三者数量关系,结合excute执行过程。
2.有界队列的线程池并且最大线程也有限制时要注意线程超出指定大小,要设置拒绝策略,否则系统会抛出RejectedExecutionException,要注意异常捕获
3.core线程数为0,最大线程为固定值的。适合针对不可控的无法预估的线程数量,且尽快处理的情况。此种线程池可实现复用。使用SynchronousQueue
线程池的关闭—shutdown
调用之后不再接收新的任务,新的任务被拒绝执行,正在执行的任务继续执行只到完成后关闭;追加的任务在线程池关闭后无法在提交
调用后新的任务被拒绝执行,正在运行的线程interrupted终止,等待的任务通过调用shutdownNow()返回
使用建议— 如何确定合适的线程数量
- 计算型任务
cpu数量的1~2倍 - I/O型任务
相对于计算型任务,需要多一些的线程,要根据具体的io阻塞市场进行综合考虑。如tomcat中默认的最大线程数:200;也可考虑根据需要在这一个最小数量和最大数量之间自动增减线程数
当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整
生产环境中,一般cpu利用率达到80%左右,认为cpu得到充分利用
通过上面这些概念,我们对于线程池或许有一些了解。中间有一些本人的理解,若有不正之处请多多谅解,并欢迎批评指正。