多线程之线程池

为什么使用线程池

我们知道创建一个线程实例在时间成本、资源耗费上都很高,这在高并发场景中,断然不能频繁的创建和销毁线程实例,而是需要对已经创建好的线程实例进行复用,这就要用到线程池技术。线程池主要解决两方面的问题:

  • 提升性能。线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池尽可能的使用空闲的线程去执行任务,最大限度的对已创建的线程进行复用,使性能提升明显。
  • 线程管理。每个java线程池会保持一些基本的线程统计信息,例如完成的任务数量,空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。

线程池创建线程方式

通过Executors创建线程池

newSingleThreadExecutor()

创建只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务。

使用此线程池的特点:

  • 单线程化的线程池中的任务是按照提交顺序执行的
  • 池中唯一的线程的存活时间是无限的
  • 当池中的唯一线程正在繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的

适用场景:任务按照提交次序,一个任务一个任务的逐个执行的场景。

此线程池弊端:内部使用的是无界队列,当任务量过大时,可能会导致服务器内存资源耗尽,产生OOM异常

newFixedThreadPool(int nthreads)

该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。
使用此线程池的特点

  • 如果线程数没有达到“固定数量”,每次提交一个任务到线程池内就创建一个新线程,直到线程达到线程池固定的数量
  • 线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)

适用场景:需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定的保持一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少的分配线程。

此线程池的弊端:内部使用无线队列存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无线增大,使服务器内存资源迅速耗尽,产生OOM异常。

newCachedThreadPool()

该方法用于创建一个“可缓存线程池”,如果线程池内的某个线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。

此线程池特点

  • 在接收新的异步任务target执行目标实例时,如果池内所有的线程繁忙,此线程池就会添加新线程来处理任务
  • 此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或jvm)能够创建的最大线程数量
  • 如果部分线程空闲,也就是存量线程的数量超过了处理任务的数量,就会回收空闲(60s不执行任务)线程

适用场景
需要快速处理突发性强、耗时较短的任务场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新 线程

此线程池弊端
线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多导致内存资源耗尽,甚至导致CPU线程资源耗尽。

newScheduledThreadPool()

该方法用于创建一个“可调度线程池”,即提供一个“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。

此线程池的特点

  • 线程池中的任务是按照一定的频率定时执行的
  • 如果被调任务的执行时间大于指定的间隔时间时,ScheduledExecutorService并不会创建一个新的线程去并发执行这个任务,而是等待前一次调度执行完成。

适用场景
周期性的执行任务的场景。

再次强调,使用Executors创建线程池虽然方便,但其缺点也是显而易见甚至是致命的,因此在现实开发中禁止使用这种方式创建线程池。下面我们介绍另一种推荐使用创建线程池的方式。

通过TreadPoolExecutor创建线程池

实际开发中我们推荐使用TreadPoolExecutor来创建线程池。事实上,当你看Executors源码的时候你会发现其底层也是调用的TreadPoolExecutor来创建线程池的,只不过给不同的参数设置了不同的默认值。
看源码发现TreadPoolExecutor构造方法有多个重载版本,我们这里以最重要的一个为例来进行详解:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                              long keepAliveTime, TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

ThreadPoolExecutor构造方法参数解析:

  • corePoolSize:核心线程数
    线程池的线程一般可以分为两类:核心线程和普通线程。核心线程数控制核心线程的数量;普通线程数有最大线程数减去核心线程数控制。核心线程即便空闲的时候一般也不会被回收,但是我们也可以通过设置让核心线程和普通线程一样在空闲一段时间后被回收。
  • maximumPoolSize:最大线程数
    控制着线程池中的可以创建的最大线程数,不能小于设置的核心线程数。另外需要注意的是corePoolSize和maximumPoolSize不仅可以在线程池构造时设置,还可以在调用setCorePoolSize()和 setMaximumPoolSize()进行动态设置。
  • keepAliveTime:池内线程最大空闲时长
    如果超过了这个时长,默认情况下非Core线程会被回收。
  • unit:池内线程最大空闲时长单位
  • workQueue:阻塞队列
    如果线程池中的核心线程都在忙,那么所接收到的任务缓存在阻塞队列中。
  • threadFactory:线程创建工厂
    ThreadFactory线程工厂用于创建线程,可以更改所创建的新线程的名称、线程组、优先级、守护线程状态等。如果没有设置这个参数,系统会使用默认的线程工厂实例来创建线程。当然也可以根据我们的需要自定义线程工厂类
  • handler:拒绝策略
    在线程池的任务缓冲队列为有界队列时,如果队列满了,再提交任务到线程池就会执行此拒绝策略。

线程池的工作原理

线程池任务调度流程:

  • 如果当前工作线程数量小于核心线程数,执行器总是优先创建一个新任务线程,而不是从线程队列中获取一个空闲线程。
  • 如果线程池中总的任务数量大于核心线程线程数,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在线程池核心线程已经用完、阻塞队列没有满的情况下,线程池不会为新任务创建一个新线程。
  • 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空。
  • 在线程池核心线程数量用完、阻塞队列也已满时,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务
  • 在核心线程都用完、阻塞队列已满时,一直会创建新线程去执行新任务,直到池内的线程数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会根绝拒绝策略来处理新任务。

线程池的阻塞队列

java中的阻塞队列与普通队列相比有一个重要的特点:在阻塞队列为空时会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动唤醒(唤醒过程不需要用户程序干预)

java线程池中使用BlockingQueue实例暂时存放接收到的异步任务,BlockingQueue是一个超级接口,常用的实现类如下:

  • ArrayBlockingQueue
    一个数组实现的有界队列,队列中的元素按FIFO排序。创建时必须设置大小。
  • LinkedBlockingQueue
    一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量,不设置容量默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue。
  • PriorityBlockingQueue
    一个具有优先级的无界队列
  • DelayQueue
    一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素时,只有已经过期的元素才会出队,队列头部的元素是过期最快的元素。
  • SynchronousQueue
    一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue。这个队列比较特殊,它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。

线程池的拒绝策略

任务被拒绝有两种情况:

  • 线程池已经关闭。
  • 工作队列已满且线程超过了最大线程数。

无论哪种情况任务被拒绝,线程池都会调用拒绝策略,常用拒绝策略如下:

  • AbortPolicy
    如果线程池队列满了,新任务就会被拒绝,并抛出异常。这是默认的拒绝策略。
  • DiscardPolicy
    如果线程池队列满了,新任务就会直接被丢掉,并且不会有任何异常抛出。
  • DiscardOldestPolicy
    如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除队头元素后再尝试入队。
  • CallerRunsPolicy
    在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
  • 自定义策略

向线程池提交任务的两种方式

向线程池提交任务有两种方式:

  • execute
void execute(Runnable command);

此方法在Executor接口中。

  • submit
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

这些方法是在ExecutorService接口中,并且ExecutorService接口继承了Executor接口。
通过submit()方式获取结果:submit()方法自身不会传递结果,而是返回一个Future异步执行实例,处理过程的结果被包装到Future实例中,调用者通过Future.get()方法获取异步执行结果。
通过submit()方式捕获异常:submit()自身并不会传递异常,处理过程中的异常被包装到了Future实例中,调用者调用Future.get()方法获取执行结果时,可以捕获异步执行过程中抛出的受检异常和运行时异常,并进行对应的业务处理。
两种提交方式的区别

  • 两者所接收的参数不一样
  • submit()提交任务后会有返回值,而execute()没有。
  • submit()方便异常处理

线程池状态

线程池总共存在五种状态

  • RUNNING
    线程池创建之后的初始状态,这种状态下可以执行任务。
  • SHUTDOWN
    该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
  • STOP
    该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会所有工作线程。
  • TIDYING
    该状态下所有任务都已终止或处理完成,将会执行terminated()钩子方法。
  • TERMINATED
    执行完terminated()钩子方法之后的状态。

线程池状态转换规则

  • 线程池创建之后状态为RUNNING
  • 执行线程池的shutdown()实例方法,会使线程池状态从RUNNING转变为SHUTDOWN。
  • 执行线程池的shutdownnow()实例方法,会使线程池状态从RUNNING转变为STOP。
  • 当线程池处于SHUTDOWN状态时,执行其shutdownNow()方法会将其状态转变为STOP。
  • 等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从STOP转变为TIDYING(当然如果上一步不调用shutdownNow()方法,线程池的状态就是从SHUTDOWN变为TIDYING)。
  • 执行完terminated()钩子方法之后,线程池状态从TIDYING转变为TERMINATED

如何优雅关闭线程池

优雅关闭线程池主要涉及到三个方法:

  • shutdown()
    此方法的原理:
    ShutDown()方法首先加锁,其次检查调用者是否用于执行线程池关闭的java Security权限。接着shutDown()方法会将线程池状态变味SHUTDOWN,在这之后线程池不再接受提交的新任务。此时如果还继续往线程池提交任务,将会使用线程池拒绝策略响应,然后进行中断空闲线程,最后会执行一个内部的钩子方法,随后释放锁。注意,调用此方法后,会等待所有任务有序地执行完毕。
  • shutdownNow()
    此方法的原理:
    shutdownNow()方法首先加锁,其次检查调用者是否用于执行线程池关闭的java Security权限。接着修改线程池的状态为STOP,然后中断所有线程,包括工作线程和空闲线程,最后丢弃工作队列中的剩余任务,随后释放锁。注意,调用此方法后,会立即中断正在执行的任务,同时丢弃队列中的任务。
  • awaitTermination()
    等待线程池完成关闭。在调用线程池的shutdown()和shutdownnow()方法时,当前线程会立即返回,不会一直等待线程池完全关闭,换句话说,用户程序都不会主动等待线程池。如果需要等到线程池关闭完成,可以调用awaitTermination()。

在实际开发中,要想优雅的关闭线程池,需要上面这三个方法共同配合完成,大致分为以下几步:

  • 执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。
  • 执行awaitTermination(long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成。
  • 如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务。
  • 补充执行awaitTermination(long timeout,TimeUnit unit)方法,判断线程池是否关闭完成。如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或者循环结束。

如何设计线程池的线程数

线程池应该设置多少线程数,这其实没有规定,我们需要根据不同的业务和硬件设施来进行统筹考虑。常见的线程池的异步任务大致分为以下三类:

  • IO密集型任务
    此类任务主要是执行IO操作。由于执行IO操作的时间较长,导致CPU的利用率不高,这类任务CPU常处于空闲状态。Netty的IO处理是典型的IO密集型任务。
    线程数确认:通常需要开CPU核心数的两倍线程
  • CPU密集型任务
    此类任务主要是执行计算任务。由于响应时间很快,CPU一直在运行,这种任务CPU利用率很高。
    线程数确认:线程数等于cpu核心数。如果线程过多,就需要频繁的切换线程,线程上下文切换时需要消耗时间,反而会使得任务效率下降。
  • 混合型任务
    此类任务既要执行逻辑计算,又要进行IO操作(如RPC调用,数据库访问)。相对来说,由于执行IO操作的耗时较长(一次网络往返往往在数百毫秒级别),这类任务的CPU利用率也不是太高。web服务器的http请求处理是此类任务的典型例子。
    线程数确认:最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数

调度器的钩子方法

ThreadPoolExecutor线程池调度器为每个任务执行前后都提供了钩子方法。ThreadPoolExecutor中提供了三个空钩子方法,这三个方法一般用作被之类重写:

  • 任务执行前钩子方法
protected void beforeExecute(Thread t, Runnable r) { }
  • 任务执行后的钩子方法
protected void afterExecute(Runnable r, Throwable t) { }
  • 线程池终止时的钩子方法
protected void terminated() { }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值