Java 线程池最细的解释,看完后彻底征服面试官

用户头像
小新
关注
发布于: 2020 年 07 月 06 日

本篇文章主要介绍了线程池作用、如何创建线程池、自定义线程工厂和拒绝策略以及深入分析不推荐直接使用Executors静态工厂直接创建线程池的缘由,让大家可以对线程池有个更深刻的认识,而不是只停留在盲目去用的层面。

线程池的必要性及作用

线程能够充分合理地协调利用CPU、内存、I/O等系统资源,但是线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有空间,在线程销毁时需要回收这些系统资源。频繁地创建和销毁线程会大大浪费系统资源,这时候就需要线程池来管理线程,提高线程的复用(当然线程的作用并不仅于此)。

线程的作用:

  • 复用线程、控制最大并发数。

  • 实现定时、周期等与时间相关的功能。

  • 实现任务队列缓存策略和拒绝机制。

  • 隔离线程环境。如:文件上传服务和数据查询服务在同一台服务器上,由于文件上传服务耗时严重,如果文件上传和数据查询服务使用同一个线程池,那么文件上传服务会影响到数据查询服务。可以通过配置独立线程池来实现文件上传和数据查询服务隔离,避免两者相互影响。

如何创建线程池

JDK中提供了创建线程池的类,大家首先想到的一定是Executors类,没错,可以通过Executors类来创建线程池,但是不推荐(原因后面会分析)。Executors类只是个静态工厂,提供创建线程池的几个静态方法(内部屏蔽了线程池参数配置细节),而真正的线程池类是ThreadPoolExecutor。ThreadPoolExecutor构造方法如下:





public ThreadPoolExecutor(int corePoolSize,



int maximumPoolSize,



long keepAliveTime,



TimeUnit unit,



BlockingQueue<Runnable> workQueue,



ThreadFactory threadFactory,



RejectedExecutionHandler handler) {



if (corePoolSize < 0 ||



//maximumPoolSize必须大于或等于1也要大于或等于corePoolSize



maximumPoolSize <= 0 ||



maximumPoolSize < corePoolSize ||



keepAliveTime < 0)



throw new IllegalArgumentException();



if (workQueue == null || threadFactory == null || handler == null)



throw new NullPointerException();



this.acc = System.getSecurityManager() == null ?



null :



AccessController.getContext();



this.corePoolSize = corePoolSize;



this.maximumPoolSize = maximumPoolSize;



this.workQueue = workQueue;



this.keepAliveTime = unit.toNanos(keepAliveTime);



this.threadFactory = threadFactory;



this.handler = handler;



}

参数解释:

  1. corePoolSize:核心线程数。如果等于0,则任务执行完后,没有任务请求进入时销毁线程池中的线程。如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。设置过大会浪费系统资源,设置过小导致线程频繁创建。

  2. maximumPoolSize:最大线程数。必须大于等于1,且大于等于corePoolSize。如果与corePoolSize相等,则线程池大小固定。如果大于corePoolSize,则最多创建maximumPoolSize个线程执行任务,其他任务加入到workQueue缓存队列中,当workQueue为空且执行任务数小于maximumPoolSize时,线程空闲时间超过keepAliveTime会被回收。

  3. keepAliveTime:线程空闲时间。线程池中线程空闲时间达到keepAliveTime值时,线程会被销毁,只到剩下corePoolSize个线程为止。默认情况下,线程池的最大线程数大于corePoolSize时,keepAliveTime才会起作用。如果allowCoreThreadTimeOut被设置为true,即使线程池的最大线程数等于corePoolSize,keepAliveTime也会起作用(回收超时的核心线程)。

  4. unit:TimeUnit表示时间单位。

  5. workQueue:缓存队列。当请求线程数大于maximumPoolSize时,线程进入BlockingQueue阻塞队列。

  6. threadFactory:线程工厂。用来生产一组相同任务的线程。主要用于设置生成的线程名词前缀、是否为守护线程以及优先级等。设置有意义的名称前缀有利于在进行虚拟机分析时,知道线程是由哪个线程工厂创建的。

  7. handler:执行拒绝策略对象。当达到任务缓存上限时(即超过workQueue参数能存储的任务数),执行拒接策略,可以看做简单的限流保护。

线程池相关类结构

搜图

编辑

ExecutorService接口继承了Executor接口,定义了管理线程任务的方法。ExecutorService的抽象类AbstractExecutorService提供了submit、invokeAll()等部分方法实现,但是核心方法Executor.execute()并没有实现。因为所有任务都在这个方法里执行,不同的线程池实现策略会有不同,所以交由具体的线程池来实现。

Executors核心方法

  • Executors.newFixedThreadPool:创建固定线程数的线程池。核心线程数等于最大线程数,不存在空闲线程,keepAliveTime为0。





public static ExecutorService newFixedThreadPool(int nThreads) {



return new ThreadPoolExecutor(nThreads, nThreads,



0L, TimeUnit.MILLISECONDS,



new LinkedBlockingQueue<Runnable>());



}
  • Executors.newSingleThreadExecutor:创建单线程的线程池,核心线程数和最大线程数都为1,相当于串行执行。





public static ExecutorService newSingleThreadExecutor() {



return new FinalizableDelegatedExecutorService



(new ThreadPoolExecutor(1, 1,



0L, TimeUnit.MILLISECONDS,



new LinkedBlockingQueue<Runnable>()));



}
  • Executors.newScheduledThreadPool:创建支持定时以及周期性任务执行的线程池。最大线程数是Integer.MAX_VALUE。存在OOM风险。keepAliveTime为0,所以不回收工作线程。





public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {



return new ScheduledThreadPoolExecutor(corePoolSize);



}





public ScheduledThreadPoolExecutor(int corePoolSize) {



//ScheduledThreadPoolExecutor的父类是ThreadPoolExecutor,最大线程数为Integer.MAX_VALUE



super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,



new DelayedWorkQueue());



}
  • Executors.newCachedThreadPool:核心线程数为0,最大线程数为Integer.MAX_VALUE,是一个高度可伸缩的线程池。存在OOM风险。keepAliveTime为60,工作线程处于空闲状态超过keepAliveTime会回收线程。





public static ExecutorService newCachedThreadPool() {



return new ThreadPoolExecutor(0, Integer.MAX_VALUE,



60L, TimeUnit.SECONDS,



new SynchronousQueue<Runnable>());



}
  • Executors.newWorkStealingPool:JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争。





public static ExecutorService newWorkStealingPool() {



//默认设置CPU数量为并行度



return new ForkJoinPool



(Runtime.getRuntime().availableProcessors(),



ForkJoinPool.defaultForkJoinWorkerThreadFactory,



null, true);



}

禁止直接使用Executors创建线程池原因:

Executors.newCachedThreadPool和Executors.newScheduledThreadPool两个方法最大线程数为Integer.MAX_VALUE,如果达到上限,没有任务服务器可以继续工作,肯定会抛出OOM异常。

Executors.newSingleThreadExecutor和Executors.newFixedThreadPool两个方法的workQueue参数为new LinkedBlockingQueue<Runnable>(),容量为Integer.MAX_VALUE,如果瞬间请求非常大,会有OOM风险。

总结:以上5个核心方法除Executors.newWorkStealingPool方法之外,其他方法都有OOM风险。





public LinkedBlockingQueue() {



this(Integer.MAX_VALUE);



}





public LinkedBlockingQueue(int capacity) {



if (capacity <= 0) throw new IllegalArgumentException();



this.capacity = capacity;



last = head = new Node<E>(null);



}

如何自定义ThreadFactory





public class UserThreadFactory implements ThreadFactory {



private final AtomicInteger threadNumber = new AtomicInteger(1);



private final String namePrefix;





public UserThreadFactory() {



namePrefix = "UserThreadFactory's " + "-Worker-";



}





public Thread newThread(Runnable r) {



String name = namePrefix + threadNumber.getAndIncrement();



Thread t = new Thread(null, r, name);



if (t.isDaemon())



t.setDaemon(false);



if (t.getPriority() != Thread.NORM_PRIORITY)



t.setPriority(Thread.NORM_PRIORITY);



return t;



}



}

如上代码所示,实现ThreadFactory接口并在newThread方法中实现设置线程的名称、是否为守护线程以及线程优先级等属性。这样做有助于快速定位死锁、StackOverflowError等问题。如下图所示,绿色框自定义的线程工厂明显比蓝色的默认线程工厂创建的线程名称拥有更多的额外信息。

搜图

编辑

线程拒绝策略

ThreadPoolExecutor提供了四个公开的内部静态类:

  • AbortPolicy:默认,丢弃任务并抛出RejectedExecutionException异常。

  • DiscardPolicy:丢弃任务,但是不抛出异常(不推荐)。

  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中。

  • CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行。

友好的拒绝策略:

  • 保存到数据库进行削峰填谷。在空闲时再提出来执行。

  • 转向某个提示页面

  • 打印日志

自定义拒绝策略:





//实现RejectedExecutionHandler接口即可



public class UserRejectedHandler implements RejectedExecutionHandler {





private static final Logger logger = Logger.getLogger(UserRejectedHandler.class);





@Override



public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {



//这里还可以拿到Runnable r任务,记录到数据库中,等流量高峰过后在执行



logger.warning("task rejected" + executor.toString());



}



}

END

笔者是一位热爱互联网、热爱互联网技术、热于分享的年轻人,如果您跟我一样,我愿意成为您的朋友,分享每一个有价值的知识给您。喜欢作者的同学,点赞+转发+关注哦!

最近整理了java架构文档和学习笔记文件以及架构视频资料和高清架构进阶学习导图免费分享给大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术资料),希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习,也可以关注我一下以后会有更多干货分享。

点赞+转发+关注添加小助手VX:xuanwo008 备注好“CSDN”信息即可获得BAT大厂面试资料、高级架构师VIP视频课程等高质量技术资料。



用户头像

小新

关注

还未添加个人签名 2020.07.02 加入

还未添加个人简介

评论 (2 条评论)

发布
用户头像
资料下载部分请删除,保留不给予置顶位机会。
2020 年 07 月 06 日 11:02
回复
没有更多了
Java线程池最细的解释,看完后彻底征服面试官