线程池的使用说明

线程池主要由两个概念组成:一个是任务队列;另一个是工作者线程。工作者线程主体就是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务。

ThreadPoolExecutor

继承自AbstractExecutorService,实现了ExecutorService。

ThreadPoolExecutor实现了生产者/消费者模式,工作者线程就是消费者,任务提交者就是生产者,线程池自己维护任务队列。当我们碰到类似生产者/消费者问题时,应该优先考虑直接使用线程池,而非“重新发明轮子”,应自己管理和维护消费者线程及任务队列。

两个构造函数

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, RejectedExecutionHandler handler)

第二个构造方法多了两个参数threadFactory和handler,这两个参数一般不需要,第一个构造方法会设置默认值。

参数说明

入参

说明

corePoolSize

核心线程个数

表示线程池中的核心线程个数,不是一开始就创建这么多线程,刚创建一个线程池后,实际上并不会创建任何线程。

有新任务到来的时候,如果当前线程个数小于corePoolSiz,就会创建一个新线程来执行该任务,需要说明的是,即使其他线程现在也是空闲的,也会创建新线程。

线程个数小于等于corePoolSize时,默认情况下:

  • 核心线程不会预先创建,只有当有任务时才会创建。
  • 核心线程不会因为空闲而被终止,keepAliveTime参数不适用于它。

不过,ThreadPoolExecutor有如下方法,可以改变这个默认行为:

//预先创建所有的核心线程

public int prestartAllCoreThreads()

//创建一个核心线程,如果所有核心线程都已创建,则返回false

public boolean prestartCoreThread()

//如果参数为true,则keepAliveTime参数也适用于核心线程

public void allowCoreThreadTimeOut(boolean value)

maximumPoolSize

最大线程个数

表示线程池中的最多线程数,线程的个数会动态变化,但这是最大值,不管有多少任务,都不会创建比这个值大的线程个数。

如果线程个数大于等于corePoolSize,那就不会立即创建新线程了,它会先尝试排队,需要强调的是,它是“尝试”排队,而不是“阻塞等待”入队,如果队列满了或其他原因不能立即入队,它就不会排队,而是检查线程个数是否达到了maximumPoolSize,如果没有,就会继续创建线程,直到线程数达到maximumPoolSize。

keepAliveTime

unit

空闲线程存活时间

keepAliveTime的目的是为了释放多余的线程资源,它表示,当线程池中的线程个数大于corePoolSize时额外空闲线程的存活时间。也就是说,一个非核心线程,在空闲等待新任务时,会有一个最长等待时间,即keepAliveTime,如果到了时间还是没有新任务,就会被终止。如果该值为0,则表示所有线程都不会超时终止。

workQueue

任务队列

要求的队列类型是阻塞队列BlockingQueue。

注意:

  • 如果用的是无界队列LinkedBlockingQueue,线程个数最多只能达到corePoolSize,到达corePoolSize后,新的任务总会排队,参数maximumPoolSize也就没有意义了。
  • 对于没有实际存储空间的同步阻塞队列SynchronousQueue,当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到达到maximumPoolSize。

threadFactory

线程工厂

类型为接口ThreadFactory:

public interface ThreadFactory {

    Thread newThread(Runnable r);

}

这个接口根据Runnable创建一个Thread, ThreadPoolExecutor的默认实现是Executors类中的静态内部类DefaultThreadFactory,主要就是创建一个线程,给线程设置一个名称,设置daemon属性为false,设置线程优先级为标准默认优先级,线程名称的格式为:pool-<线程池编号>-thread-<线程编号>。

如果需要自定义一些线程的属性,比如名称,可以实现自定义的ThreadFactory。

handler

任务拒绝策略

如果队列有界,且maximumPoolSize有限,则当队列排满,线程个数也达到了maximumPoolSize,这时,新任务来了,会触发线程池的任务拒绝策略。

ThreadPoolExecutor实现了4种处理方式:

  1. ThreadPoolExecutor.AbortPolicy:这就是默认的方式,抛出异常。
  2. ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛出异常,也不执行。
  3. ThreadPoolExecutor.DiscardOldestPolicy:将等待时间最长的任务扔掉,然后自己排队。
  4. ThreadPoolExecutor.CallerRunsPolicy:在任务提交者线程中执行任务,而不是交给线程池中的线程执行。

这些拒绝策略都实现RejectedExecutionHandler接口:

public interface RejectedExecutionHandler {

    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);

}

当线程池不能接受任务时,调用其拒绝策略的rejectedExecution方法。

注意:

  • 拒绝策略只有在队列有界,且maximumPoolSize有限的情况下才会触发。
  • 如果队列无界,服务不了的任务总是会排队,但这不一定是期望的结果,因为请求处理队列可能会消耗非常大的内存,甚至引发内存不够的异常。
  • 如果队列有界但maximumPoolSize无限,可能会创建过多的线程,占满CPU和内存,使得任何任务都难以完成。

所以,在任务量非常大的场景中,让拒绝策略有机会执行是保证系统稳定运行很重要的方面。

查看关于线程和任务数的一些动态数字:

获取状态函数

说明

public int getPoolSize()

返回当前线程个数

public int getLargestPoolSize()

返回线程池曾经达到过的最大线程个数

public long getCompletedTaskCount()

返回线程池自创建以来所有已完成的任务数

public long getTaskCount()

返回所有任务数,包括所有已完成的加上所有排队待执行的

工厂类Executors

类Executors提供了一些静态工厂方法,可以方便地创建一些预配置的线程池,主要方法有:

public static ExecutorService newSingleThreadExecutor()
	
public static ExecutorService newFixedThreadPool(int nThreads)
	
public static ExecutorService newCachedThreadPool()

方法

说明

使用场景

newSingleThreadExecutor

只使用一个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止,该线程顺序执行所有任务。

需要确保所有任务被顺序执行的场合。

newFixedThreadPool

使用固定数目的n个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止。和newSingleThreadExecutor一样,由于是无界队列,如果排队任务过多,可能会消耗过多的内存。

在系统负载很高的情况下,newFixedThreadPool可以通过队列对新任务排队,保证有足够的资源处理实际的任务。

newCachedThreadPool

它的corePoolSize为0, maximumPoolSize为Integer.MAⅩ_VALUE, keepAliveTime是60秒,队列为SynchronousQueue。

它的含义是:当新任务到来时,如果正好有空闲线程在等待任务,则其中一个空闲线程接受该任务,否则就总是创建一个新线程,创建的总线程个数不受限制,对任一空闲线程,如果60秒内没有新任务,就终止。

如果系统负载不太高,单个任务的执行时间也比较短,newCachedThreadPool的效率可能更高,因为任务可以不经排队,直接交给某一个空闲线程。

注意:在系统负载可能极高的情况下,上述方式都不是好的选择,newFixedThreadPool的问题是队列过长,而newCachedThreadPool的问题是线程过多,这时,应根据具体情况自定义ThreadPoolExecutor,传递合适的参数。

#java原理##并发编程#
27届毕业生-Java知识专辑 文章被收录于专栏

知其然知其所以然,只有掌握了底层原理,借助第一性原理,才可以在日常开发和项目中运用自如,潇洒走江湖。 专为27届毕业生准备,托起您的就业梦。 该专辑会不定时更新,建议27届同学订阅,入职后扎实的基本功可以帮您争取更好的机会和项目。

全部评论
看完这篇文档,来检验下你的掌握情况,看看你是否可以找出其中的问题:https://www.nowcoder.com/discuss/859437338936487936
点赞 回复 分享
发布于 今天 14:26 北京

相关推荐

评论
点赞
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务