ThreadPool线程池详解-多线程系列

线程池

JDK中JUC包多线程内容的讲解,涉及「线程池、阻塞队列、协作器、ThreadLocal、Atomic、Future、锁、CAS、AQS 等」; Object 下wait、notify ;JVM层面 synchronized、volatile 等。 最新文章公众号持续更新中… 欢迎骚扰,分享技术,探讨生活,
ThreadPool线程池详解-多线程系列_第1张图片

线程池简介

线程池,顾名思义是一个放着线程的池子,池子中的线程主要是用来执行任务的。当用户提交任务时,线程池会创建线程去执行任务,若任务超过了核心线程数的时候,会在一个任务队列里进行排队等待(详细往下在讲)。任务,通常是一些大批量的或者耗时的工作单元,会把应用程序的工作分解到多个任务中去执行。一般需要使用多线程执行任务的时候,这些任务最好都是相互独立的,这样有一定的任务边界供程序把控。 多线程,当使用多线程的时候,任务处理过程就可以从主线程中分离出来,任务可以并行处理,同时处理多个请求。任务处理代码必须是线程安全的

为何要使用线程池?

  1. 降低开销:在创建和销毁线程的时候会产生很大的系统开销,频繁创建/销毁意味着CPU资源的频繁切换和占用,线程是属于稀缺资源,不可以频繁的创建。假设创建线程的时长记为10ms,线程执行任务的时长记为15ms,销毁线程的时长记为10ms,如果我们执行任务15ms<10ms+10ms,这样的开销是不划算的,不使用线程池去避免创建和销毁的开销,会极大的资源浪费。
  2. 易复用和管理:将线程都放在一个池子里,便于统一管理(可以延时执行,可以统一命名线程名称(便于排查问题),等),同时,也便于任务进行复用。
  3. 解耦:将线程的创建和销毁与执行任务完全分离出来,这样方便于我们进行维护,也让我们更专注于业务开发。

线程池的优势

  1. 提高资源的利用性:通过池化可以重复利用已创建的线程,空闲线程可以处理新提交的任务,从而降低了创建和销毁线程的资源开销。
  2. 提高线程的管理性:在一个线程池中管理执行任务的线程,对线程可以进行统一的创建、销毁以及监控等,对线程数做控制,防止线程的无限制创建,避免线程数量的急剧上升而导致CPU过度调度等问题,从而更合理的分配和使用内核资源。
  3. 提高程序的响应性:提交任务后,有空闲线程可以直接去执行任务,无需新建。
  4. 提高系统的可扩展性:利用线程池可以更好的扩展一些功能,比如定时线程池可以实现系统的定时任务。

线程池记忆口诀

七个参数,四大特性,五个种类、三大工作队列、四大拒绝策略

1.七个参数

一共有7个:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler

int corePoolSize:该线程池中核心线程数最大值

这边我们区分两个概念:

  • 核心线程:线程池新建线程的时候,当前活动的线程总数< corePoolSize,新建的线程即为核心线程。
  • 非核心线程:线程池新建线程的时候,当前活动的线程总数> corePoolSize, 且阻塞队列已满,这时新建一个线程来执行新提交的任务即为非核心线程。

核心线程默认情况下会一直存活在线程池中,即使这个核心线程不工作(空闲状态),除非ThreadPoolExecutor 的 allowCoreThreadTimeOut这个属性为 true,那么核心线程如果空闲状态下,超过一定时间后就被销毁。

int maximumPoolSize:线程总数最大值

线程总数 = 核心线程数 + 非核心线程数

long keepAliveTime:非核心线程空闲超时时间

keepAliveTime即为空闲线程允许的最大的存活时间。如果一个非核心线程空闲状态的时长超过keepAliveTime了,就会被销毁掉。注意:如果设置allowCoreThreadTimeOut = true,就变成核心线程超时销毁了。

TimeUnit unit:是keepAliveTime 的单位

TimeUnit 是一个枚举类型,列举如下:

ThreadPool线程池详解-多线程系列_第2张图片

2.四大特性

ThreadPool线程池详解-多线程系列_第3张图片

1.当前线程数量未达到 corePoolSize,则新建一个线程(核心线程)执行任务

2.当前线程数量达到了 corePoolSize,则将任务移入阻塞队列等待,让空闲线程处理;

3.当阻塞队列已满新建线程(非核心线程)执行任务

4.当阻塞队列已满,总线程数又达到了 maximumPoolSize,就会按照拒绝策略处理无法执行的任务,比如RejectedExecutionHandler抛出异常。

3.五个种类

  • 常用四种

创建固定数目线程的线程池 Executors.newFixedThreadPool(200);

创建一个无限线程的线程池,无需等待队列,任务提交即执行 Executors.newCachedThreadPool()

创建有且仅有一个线程的线程池Executors.newSingleThreadExecutor();

创建一个定时周期执行的线程池Executors.newScheduledThreadPool();

ThreadPool线程池详解-多线程系列_第4张图片

注意事项

1.FixedTheradPool和SingleThreadedPool因为是固定core和max,所以其阻塞任务队列是LinkedBlockingQueue没有边界的所以任务过多处理不完会导致OOM

2.CachedThreadPool和S cheduledThreadPool因为max是Integer.MAX_VALUE,阻塞队列是固定长度的ArrayBlockingQueue所以在队列满之后一直创建新线程可能会导致OOM

PS: 阿里禁止使用Executors线程池工具创建线程池,要使用ThreadPoolExecutor根据构造函数应用场景硬件等自定义线程池运行规则

4.三大工作队列

1.无界队列 LinkedBlockingQueue

2.有界队列 ArrayBlockingQueue

3.直接交界 SynchronousQueue

5.四大拒绝策略

ThreadPoolExecutor的拒绝策略可以通过调用setRejectedExecutionHandler来修改。JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。 拒绝策略如下:

RejectedExecutionHandler rejected = null;

//默认策略,阻塞队列满,则丢任务、抛出异常
rejected = new ThreadPoolExecutor.AbortPolicy();

//阻塞队列满,则丢任务,不抛异常
rejected = new ThreadPoolExecutor.DiscardPolicy();

//删除队列中最旧的任务(最早进入队列的任务),尝试重新提交新的任务
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();

//队列满,不丢任务,不抛异常,若添加到线程池失败,那么主线程会自己去执行该任务
rejected = new ThreadPoolExecutor.CallerRunsPolicy();

(1)AbortPolicy、DiscardPolicy和DiscardOldestPolicyAbortPolicy默认的饱和策略,就是中止任务,该策略将抛出RejectedExecutionException。调用者可以捕获这个异常然后去编写代码处理异常。   当新提交的任务无法保存到队列中等待执行时,DiscardPolicy会悄悄的抛弃该任务DiscardOldestPolicy则会抛弃最旧的(下一个将被执行的任务),然后尝试重新提交新的任务。如果工作队列是那个优先级队列时,搭配DiscardOldestPolicy饱和策略会导致优先级最高的那个任务被抛弃,所以两者不要组合使用。

(2)CallerRunsPolicy  CallerRunsPolicy是“调用者运行”策略,实现了一种调节机制 。它不会抛弃任务,也不会抛出异常。 而是将任务回退到调用者。它不会在线程池中执行任务,而是在一个调用了execute的线程中执行该任务。在线程满后,新任务将交由调用线程池execute方法的主线程执行,而由于主线程在忙碌,所以不会执行accept方法,从而实现了一种平缓的性能降低。   当工作队列被填满后,没有预定义的饱和策略来阻塞execute(除了抛弃就是中止还有去让调用者去执行)。然而可以通过Semaphore来限制任务的到达率。

线程池运行状态

RUNNING:运行状态,指可以接受任务并执行队列里的任务。

SHUTDOWN:调用了 shutdown() 方法,不再接受新任务,但队列里的任务会执行完毕。

STOP:指调用了 shutdownNow() 方法,不再接受新任务,所有任务都变成STOP状态,不管是否正在执行。该操作会抛弃阻塞队列里的所有任务并中断所有正在执行任务。

TIDYING:所有任务都执行完毕,程序调用 shutdown()/shutdownNow() 方法都会将线程更新为此状态,若调用shutdown(),则等执行任务全部结束,队列即为空,变成TIDYING状态;调用shutdownNow()方法后,队列任务清空且正在执行的任务中断后,更新为TIDYING状态。

TERMINATED:终止状态,当线程执行 terminated() 后会更新为这个状态。

线程池体系结构

ThreadPool线程池详解-多线程系列_第5张图片

顶层接口Executor只有一个方法executor

ThreadPool线程池详解-多线程系列_第6张图片

核心源码分析(有没有像上边执行任务的四个规则)

ThreadPool线程池详解-多线程系列_第7张图片

runwork简单梳理【runwork 线程复用原理: getTask从阻塞队列拿任务 task.run执行while循环 work不会停止一直在取任务,并且执行任务会加锁;前beforeExecuter 后afterExecute 钩子函数控制线程池中线程执行情况,并且能够对线程执行前/后进行处理】

ThreadPool线程池详解-多线程系列_第8张图片

Execute源码详细分析推荐 https://juejin.cn/post/6844904070906380301

ExecutorService线程池管理

ExecutorService继承Executor有几个Executor没有的几个方法

比如:shutdown 、shutdownNow、 isshutdown、 isTerminated、 awaitTermination 、 subimit 、 invokall 等初步管理线程池方法

  //关闭线程池,但阻塞队列中的任务继续执行完之后才关闭
  void shutdown();
  //关闭线程池,阻塞队列中的任务直接丢弃
  List<Runnable> shutdownNow();
  //是否处于SHUTDOWN状态
  boolean isShutdown();
  //是否处于TERMINATED状态
  boolean isTerminated();
  //等待线程池进入TERMINATED状态
  boolean awaitTermination( long timeout, TimeUnit unit)
  throws InterruptedException;
  //提交任务,不过任务有返回值
  <T> Future<T> submit(Callable<T> task);
  //提交任务,不过任务有返回值
  <T> Future<T> submit(Runnable task, T result);
  //提交Runnable任务,会自动封装成Callable任务
  Future<?> submit(Runnable task);

AbstractExecutorService线程池管理

这个类是一个抽象类,实现了ExecutorService,主要是为该接口的方法提供一些默认的实现。

ThreadPoolExecutor创建线程池核心方法

这个类就是线程池实现的核心类,线程池的线程管理和阻塞队列管理都是它完成的

线程数设定规则

**CPU 密集型任务(N+1):**比如圆周率计算

这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

**I/O 密集型任务(2N):**比如大文件耗时处理

这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。

你可能感兴趣的:(多线程,JUC,java)