Java面试题,Java,面试题,线程池

7 篇文章 0 订阅
1 篇文章 0 订阅

首先要注意的概念

首先线程池有几个概念大家要理解:
1、线程池是往里面放任务,不是从里面拿一个线程来执行任务。
2、线程的创建和销毁是一个重资源,JVM 中的线程与操作系统的线程是一对一的关系。
所以,这就是为什么核心线程数要存在。因为要避免线程频繁地创建与销毁,因此我们需要缓存一批线程,让它们时刻准备着执行任务。

微信交流群:Java技术沟通群⑤(点击加入)

JUC-线程池架构图

架构图

JUC就是java.util.concurrent工具包的简称,该工具包是从JDK 1.5开始加入JDK的,是用于完成高并发、处理多线程的一个工具包。

1.Executor

Executor是Java异步目标任务的“执行者”接口,其目标是执行目标任务。“执行者”Executor提供了execute()接口来执行已提交的Runnable执行目标实例。Executor作为执行者的角色,其目的是提供一种将“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:

void execute(Runnable command) 

2.ExecutorService

ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务接”口,对外提供异步任务的接收服务。ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系列方法、invoke系列方法等,具体如下:

//向线程池提交单个异步任务 
<T> Future<T> submit(Callable<T> task); 

//向线程池提交批量异步任务 
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; 

3.AbstractExecutorService

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService存在的目的是为ExecutorService中的接口提供默认实现。

4.ThreadPoolExecutor

ThreadPoolExecutor就是大名鼎鼎的“线程池”实现类,它继承于AbstractExecutorService抽象类。

ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。

5.ScheduledExecutorService

ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时”和“周期性”任务的调度线程池接口,其功能和Timer/TimerTask类似。

6.ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor的性能要优于Timer。

7.Executors

Executors是一个静态工厂类,它通过静态工厂方法返回ExecutorServiceScheduledExecutorService等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。

Executors的4种快捷创建线程池的方法

线程池的标准创建方式

注意:现实编码过程中,禁止使用快捷线程池,通过标准构造器ThreadPoolExecutor去构造工作线程池。

// 使用标准构造器构造一个普通的线程池 
public ThreadPoolExecutor( int corePoolSize, // 核心线程数,即使线程空闲(Idle),也不会回收 
int maximumPoolSize, // 线程数的上限 
long keepAliveTime, TimeUnit unit, // 线程最大空闲(Idle)时长 
BlockingQueue<Runnable> workQueue, // 任务的排队队列 
ThreadFactory threadFactory, // 新线程的产生方式 
RejectedExecutionHandler handler) // 拒绝策略

1.核心和最大线程数量

参数corePoolSize用于设置核心(Core)线程池数量,参数maximumPoolSize用于设置最大线程数量。线程池执行器将会根据corePoolSizemaximumPoolSize自动维护线程池中的工作线程,大致规则为:
(1)当在线程池接收到新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePoolSize
(2)如果当前工作线程数多于corePoolSize数量,但小于maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSizemaximumPoolSize相同,可以创建一个固定大小的线程池。
(3)当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
(4)corePoolSizemaximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize()和setMaximumPoolSize()两个方法进行动态更改。

2.BlockingQueue

BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。

3.keepAliveTime

线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时长(或者说保活时长),如果超过这个时间,默认情况下Idle、非Core线程会被回收。
如果池在使用过程中提交任务的频率变高,也可以调用方法setKeepAliveTime(long,TimeUnit)进行线程存活时间的动态调整,可以将时长延长。如果需要防止Idle线程被终止,可以将Idle时间设置为无限大,具体如下:

setKeepAliveTime(Long.MAX_VALUE,TimeUnit.NANOSECONDS); 

注意::默认情况下,Idle超时策略仅适用于存在超过corePoolSize线程的情况。但若调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数true,则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。

线程池的任务调度流程

(1)如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
(2)如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。
(3)当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
(4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。

如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize

(5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
在这里插入图片描述

四种拒绝策略

/**
 * 四种拒绝策略:
 *
 * new ThreadPoolExecutor.AbortPolicy() 
 * 默认。拒绝这个任务,并且抛出RejectedExecutionException异常。
 * 
 * new ThreadPoolExecutor.DiscardPolicy() 
 * 队列满了,丢掉任务,不会抛出异常!
 *
 * new ThreadPoolExecutor.DiscardOldestPolicy() 
 * 队列满了,尝试去和最早的竞争,也不会抛出异常!
 * 抛弃最老任务策略,也就是说如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列
 * 是队尾进 队头出,队头元素是最老的,所以每次都是移除队头元素后再尝试入队
 *
 * new ThreadPoolExecutor.CallerRunsPolicy() 
 * 调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程     
 * 去执行 新任务。
 *
 */

ThreadFactory(线程工厂)

Executors为线程池工厂类,用于快捷创建线程池(Thread Pool);ThreadFactory为线程工厂类,用于创建线程(Thread)。

任务阻塞队列

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

Java线程池使用BlockingQueue实例暂时接收到的异步任务, BlockingQueue是JUC包的一个超级接口,比较常用的实现类有:

ArrayBlockingQueue

(1)ArrayBlockingQueue:是一个数组实现的有界阻塞队列(有界队列),队列中的元素按FIFO排序ArrayBlockingQueue在创建时必须设置大小,接收的任务超出corePoolSize数量时,任务被缓存到该阻塞队列中,任务缓存的数量只能为创建时设置的大小,若该阻塞队列已满,则会为新的任务创建线程,直到线程池中的线程总数大于maximumPoolSize

LinkedBlockingQueue

(2)LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue

如果不设置LinkedBlockingQueue的容量(无界队列),当接收的任务数量超出corePoolSize时,则新任务可以被无限制地缓存到该阻塞队列中,直到资源耗尽。有两个快捷创建线程池的工厂方法 Executors.newSingleThreadExecutorExecutors.newFixedThreadPool使用了这个队列,并且都没有设置容量(无界队列)。

PriorityBlockingQueue

(3)PriorityBlockingQueue:是具有优先级的无界队列。

DelayQueue

(4)DelayQueue:这是一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,队列头部的元素是过期最快的元素。快捷工厂方法 Executors.newScheduledThreadPool所创建的线程池使用此队列。

SynchronousQueue

(5)SynchronousQueue(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue。快捷工厂方法Executors.newCachedThreadPool所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。

调度器的钩子方法

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

//任务执行之前的钩子方法(前钩子) 
protected void beforeExecute(Thread t, Runnable r) { } 
//任务执行之后的钩子方法(后钩子) 
protected void afterExecute(Runnable r, Throwable t) { } 
//线程池终止时的钩子方法(停止钩子) 
protected void terminated() { }

线程池的五种状态

线程池总共存在5种状态,定义在ThreadPoolExecutor类中,具体代码如下:

// 省略import 
public class ThreadPoolExecutor extends AbstractExecutorService { 
// runState is stored in the high-order bits 
private static final int RUNNING                = -1 << COUNT_BITS; 
private static final int SHUTDOWN                = 0 << COUNT_BITS; 
private static final int STOP                    = 1 << COUNT_BITS; 
private static final int TIDYING                = 2 << COUNT_BITS; 
private static final int TERMINATED                = 3 << COUNT_BITS; 
// 省略其他 }

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

优雅地关闭线程池主要涉及的方法有3个:
(1)shutdown
(2)shutdownNow
(3)awaitTermination
(4)注册JVM钩子函数自动关闭线程池

Executors快捷创建线程池存在潜在问题

1.使用Executors创建“固定数量的线程池”的潜在问题

newFixedThreadPool工厂方法返回一个ThreadPoolExecutor实例,该线程池实例的corePoolSize数量为参数nThread,其maximumPoolSize数 量也为参数nThread,其workQueue属性的值为 LinkedBlockingQueue<Runnable>()无界阻塞队列。

使用Executors创建“固定数量的线程池”的潜在问题主要存在于其workQueue,其值为**LinkedBlockingQueue**(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列中大量的任务等待。如果队列很大,很有可能导致JVM出现OOM(Out Of Memory)异常,即内存资源耗尽。

{ 
return new ThreadPoolExecutor( 
nThreads, // 核心线程数 
nThreads, // 最大线程数 
0L, // 线程最大空闲(Idle)时长 
TimeUnit.MILLISECONDS, // 时间单位:毫秒 
new LinkedBlockingQueue<Runnable>() //任务的排队队列,无界队列 
); 
} 

2.使用Executors创建“单线程化线程池”的潜在问题

使用newSingleThreadExecutor工厂方法创建“单线程化线程池”的源码如下:

public static ExecutorService newSingleThreadExecutor() 
{
return new FinalizableDelegatedExecutorService 
(new ThreadPoolExecutor(
1, // 核心线程数 
1, // 最大线程数 
0L, // 线程最大空闲(Idle)时长
TimeUnit.MILLISECONDS, //时间单位:毫秒 
new LinkedBlockingQueue<Runnable>() //无界队列 
)); 
}

使用Executors创建的“单线程化线程池”与“固定大小的线程池”一样,其潜在问题仍然存在于其workQueue属性上,该属性的值为LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列大量阻塞。如果队列很大,很有可能导致JVM的OOM异常,甚至造成内存资源耗尽。

3.使用Executors创建“可缓存线程池”的潜在问题

public static ExecutorService newCachedThreadPool() { 
return new ThreadPoolExecutor( 
0, // 核心线程数 
Integer.MAX_VALUE, // 最大线程数 
60L, // 线程最大空闲(Idle)时长 
TimeUnit.MILLISECONDS, // 时间单位:毫秒 
new SynchronousQueue<Runnable>() // 任务的排队队列,无界队列 
); }

newCachedThreadPool()当“可缓存线程池”有新任务到来时,新任务会被插入SynchronousQueue实例中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程,则线程池会创建一个线程来执行该任务。

使用Executors创建的“可缓存线程池”newCachedThreadPool() 的潜在问题存在于其最大线程数量不设限上。由于其maximumPoolSize的值为Integer.MAX_VALUE(非常大),可以认为可以无限创建线程,如果任务提交较多,就会造成大量的线程被启动,很有可能造成OOM异常,甚至导致CPU线程资源耗尽。

4.使用Executors创建“可调度线程池”的潜在问题

使用newScheduledThreadPool工厂方法创建“可调度线程池”的:

public ScheduledThreadPoolExecutor(int corePoolSize) {
super(
corePoolSize, // 核心线程数 
Integer.MAX_VALUE, // 最大线程数 
0, // 线程最大空闲(Idle)时长 
NANOSECONDS,//时间单位 
new DelayedWorkQueue() //任务的排队队列 
); }

其corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE,表示线程数不设上限,其workQueue为一个DelayedWorkQueue实例,这是一个按到期时间升序排序的阻塞队列

使用Executors创建的“可缓存线程池”的潜在问题存在于其最大线程数量不设限上。由于其线程数量不设限,如果到期任务太多,就会导致 CPU的线程资源耗尽。

总结

总结起来,使用Executors创建线程池主要的弊端如下:

(1)FixedThreadPool和SingleThreadPool 这两个工厂方法所创建的线程池,工作队列(任务排队的队列)的长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM(即耗尽内存资源)。

(2)CachedThreadPool和ScheduledThreadPool 这两个工厂方法所创建的线程池允许创建的线程数量为 Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。 虽然Executors工厂类提供了构造线程池的便捷方法,但是对于服务 器程序而言,大家应该杜绝使用这些便捷方法,而是直接使用线程池 ThreadPoolExecutor的构造器,从而有效避免由于使用无界队列可能导致的内存资源耗尽,或者由于对线程个数不做限制而导致的CPU资源耗尽等问题。

所以,大厂的编程规范都不允许使用Executors创建线程池,而是要求使用标准构造器ThreadPoolExecutor创建线程池。

如何确定线程池的线程数

IO密集型任务

IO密集型任务(此类任务主要是执行IO操作。/IO事件处理线程数默认值为CPU核数的两倍;Netty的IO读写操作。)、

CPU密集型任务

CPU密集型任务(此类任务主要是执行计算任务。其特点是要进行大量计算而需要消耗CPU资源,比如计算圆周率、对视频进行高清解码等。

但是并行的任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以要最高效地利用CPU, CPU密集型任务并行执行的数量=CPU的核心数。

混合型任务

混合型任务(最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1) * CPU核数 )
CPU的核心数:Runtime.getRuntime().availableProcessors() * 2

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值