java并发编程(三)——线程池相关类Executer

线程池的相关概念

什么是线程池?

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。然而,增加可用线程数量是可能的。线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到池子中并等待下一次分配任务。

线程池的好处?

1、减少系统因为频繁创建和销毁线程所带来的开销;
2、自动管理线程,对使用方透明,使其可以专注于任务的构建。

ThreadPoolExecutor

ThreadPoolExecutor参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
 			//省略若干代码
    }

corePoolSize: 核心线程数
maximumPoolSize: 最大线程数
keepAliveTime: 空闲线程的存活时间
unit: keepAliveTime的单位
workQueue:任务队列,保存已经提交但尚未被执行的线程
threadFactory:线程工厂(用于指定如果创建一个线程)
handler:拒绝策略 (当任务太多导致工作队列满时的处理策略)

线程池的调度流程

流程图如下:
在这里插入图片描述
主要流程如下:
1、当工作线程数<核心线程数,提交任务则创建一个新的线程去执行任务
2、如果工作线程数>核心线程数,则判断任务队列workQueue是否已经满了,如果没满,则将提交的任务放到任务队列中
3、如果任务队列workQueue已经满了,判断工作线程数是否大于最大线程数,如果大于最大线程数,则执行拒绝策略
4、如果任务队列workQueue已经满了,但是工作线程数小于最大线程数,则创建新的线程,执行任务。
5、当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。
6、当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。

任务队列

线程池都有哪几种工作队列?

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • DelayQueue
  • PriorityBlockingQueue
  • SynchronousQueue

ArrayBlockingQueue:ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
LinkedBlockingQueue:LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列。
DelayQueue:DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
SynchronousQueue:
SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。

拒绝策略

ThreadPoolExecutor在以下两种情况下会执行拒绝策略:
1、当核心线程池满了以后,如果任务队列也满了,首先判断非核心线程池有没满,没有满就创建一个工作线程(归属非核心线程池), 否则就会执行拒绝策略;
2、提交任务时,ThreadPoolExecutor已经关闭了。

AbortPolicy(默认)

处理方式:如果元素添加到线程池失败,会抛出异常。

CallerRunsPolicy

处理方式:如果添加失败,那么主线程会自己调用执行器中的execute方法来执行任务。即交给线程池调用所在的线程进行处理。

DiscardPolicy

处理方式:如果添加失败,则放弃,并不会抛出任何异常。

DiscardOldestPolicy

处理方式:当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入。

Executers工具类

JDK1.5时,J.U.C中还提供了一个Executors类,专门用于创建上述接口的实现类对象。Executors其实就是一个简单工厂,它的所有方法都是static的,用户可以根据需要,选择需要创建的执行器实例,Executors一共提供了五类可供创建的Executor执行器实例。

newFixedThreadPool(固定数目线程的线程池)

 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
线程池特点:
  • 核心线程数和最大线程数大小一样
  • 没有所谓的非空闲时间,即keepAliveTime为0
  • 阻塞队列为无界队列LinkedBlockingQueue
工作机制:

在这里插入图片描述

  • 提交任务
  • 如果线程数少于核心线程,创建核心线程执行任务
  • 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  • 如果线程执行完任务,去阻塞队列取任务,继续执行。
使用场景:
FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

newSingleThreadExecutor(单线程的线程池)

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
线程池特点:
  • 核心线程数为1
  • 最大线程数也为1
  • 阻塞队列是LinkedBlockingQueue
  • keepAliveTime为0
工作机制:

在这里插入图片描述

  • 提交任务
  • 线程池是否有一条线程在,如果没有,新建线程执行任务
  • 如果有,讲任务加到阻塞队列
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。
  • 任务执完毕,这唯一的线程也不会释放。
使用场景:
适用于串行执行任务的场景,一个任务一个任务地执行。

newCachedThreadPool(可缓存线程的线程池)

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
线程池特点:
  • 核心线程数为0
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒

当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

工作机制:

在这里插入图片描述

  • 提交任务
  • 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。
使用场景:
适合任务数比较密集,但每个任务执行时间较短的情况

newScheduledThreadPool(定时及周期执行的线程池)

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
线程池特点:
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行
工作机制:
  • 添加一个任务
  • 线程池中的线程从 DelayQueue 中取任务
  • 线程从 DelayQueue 中获取 time 大于等于当前时间的task
  • 执行完后修改这个 task 的 time 为下次被执行的时间
  • 这个 task 放回DelayQueue队列中
使用场景:
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 核心线程数量固定、非核心线程数量无限制(闲置时马上回收)。

线程池状态

在这里插入图片描述

线程池有这几个状态:
RUNNING:

  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  • 调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN:

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP:

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 线程池中执行的任务为空,进入TIDYING状态;

TIDYING:

  • 该状态表明所有的任务已经运行终止,记录的任务数量为0。
  • terminated()执行完毕,进入TERMINATED状态。

TERMINATED:

  • 该状态表示线程池彻底终止。

Fork/Join线程池

Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。Fork/Join框架要完成两件事情:

1.任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割

2.执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。

ForkJoinPool

ForkJoinPool的主要工作如下:

1、接受外部任务的提交(外部调用ForkJoinPool的invoke/execute/submit方法提交任务);
2、接受ForkJoinTask自身fork出的子任务的提交;
3、任务队列数组(WorkQueue[])的初始化和管理;
4、工作线程(Worker)的创建/管理。

注意:ForkJoinPool提供了3类外部提交任务的方法:invoke、execute、submit,它们的主要区别在于任务的执行方式上。
  • 通过invoke方法提交的任务,调用线程直到任务执行完成才会返回,也就是说这是一个同步方法,且有返回结果;
  • 通过execute方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且没有返回结果;
  • 通过submit方法提交的任务,调用线程会立即返回,也就是说这是一个异步方法,且有返回结果(返回Future实现类,可以通过get获取结果)。

常见问题

按线程池内部机制,当提交新任务时,有哪些异常要考虑?

  1. 在任务代码try/catch捕获异常
  2. 通过Future对象的get方法接收抛出的异常,再处理
  3. 为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常
  4. 重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用

使用无界队列的线程池会导致内存飙升吗?

会的。newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值