Java并发(10)-JUC线程池 Executor框架

文章目录

    • 一. 为什么使用线程池?以及使用场景
    • 二. 线程池的基本概念
      • 2.1 线程池执行流程
      • 2.2 线程池的5种状态
      • 2.3 线程池的使用
        • 2.3.1 创建线程池的方式 以及各参数含义
        • 2.3.2 线程池的4种拒绝策略
        • 2.3.3 线程池的4种阻塞队列
        • 2.3.4 向线程池提交任务:execute() 和 submit()
        • 2.3.5 关闭线程池:shutdown()和 shutdownNow()
    • 三.Executor 框架
      • 3.1 Executor 框架结构与执行流程
      • 3.2 4种常用的ThreadPoolExecutor
        • 3.2.1 FixedThreadPool
        • 3.2.2 SingleThreadExecutor
        • 3.2.3 CachedThreadPool
      • 3.3 Executor 源码分析

一. 为什么使用线程池?以及使用场景

  1. 常见的new Thread 方式弊端:
new Thread(new Runnable() {
  
    @Override
    public void run() {
        // TODO Auto-generated method stub
    }
}).start();

这种方式的主要不方便就是:每次new Thread新建对象性能差。线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。缺乏更多功能,如定时执行、定期执行、线程中断。

因此引出线程池
第一:降低资源消耗 。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度 。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性 。使用线程池可以进行统一分配、调优和监控。可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。

二. 线程池的基本概念

对于线程池,可以通俗的将它理解为"存放一定数量线程的一个线程集合。线程池允许同时运行的线程数量就是线程池的容量;当添加的到线程池中的线程超过它的容量时,会有一部分线程阻塞等待。线程池会通过相应的调度策略和拒绝策略,对添加到线程池中的线程进行管理。

2.1 线程池执行流程

以下内容引自 《java 并发编程的艺术》

  1. 当向一个线程池提交一个新任务时,线程池的处理流程如下:
    Java并发(10)-JUC线程池 Executor框架_第1张图片

1,线程池判断核心线程池里面的线程是否都在执行任务,如果核心线程未满,则创建一个新的工作线程来执行任务。如果核心线程已满,则进如下一个流程判断。
2,线程池判断工作队列是否已经满了,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列已经满了,则进入下个流程。
3,线程池判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则提交给饱和策略来处理这个任务。

  1. ThreadPoolExecutor执行execute方法,有如下四种情况:
    (1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(需要获取全局锁)。
    (2)如果运行的线程等于或者多余corePoolSize,则将任务加入BlockingQueue。
    (3)如果无法将任务加入BlockingQueue,则创建新的线程来处理任务(需要获取全局锁)。
    (4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectExecutionHandler.rejectedExecution方法。
    Java并发(10)-JUC线程池 Executor框架_第2张图片
    线程池采取上述步骤的总体设计思路,是为了执行execute方法时候,尽可能避免获取全局锁。在ThreadPoolExecutor完成预热之后(即当前运行的线程数目大于等于核心线程数目),几乎所有的execute方法调用的认为有都是等待队列中的任务。

2.2 线程池的5种状态

线程有5种状态:新建状态,就绪状态,运行状态,阻塞状态,死亡状态。线程池也有5种状态;然而,线程池不同于线程,线程池的5种状态是:Running, SHUTDOWN, STOP, TIDYING, TERMINATED。

以下内容引自 Java多线程系列–“JUC线程池”04之 线程池原理(三)

Java并发(10)-JUC线程池 Executor框架_第3张图片

  1. RUNNING
    (01) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
    (02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态!
    道理很简单,在ctl的初始化代码中(如下),就将它初始化为RUNNING状态,并且"任务数量"初始化为0。

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    
  2. SHUTDOWN
    (01) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
    (02) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING ->SHUTDOWN。

  3. STOP
    (01) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
    (02) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

  4. TIDYING
    (01) 状态说明:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
    (02) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

  5. TERMINATED
    (01) 状态说明:线程池彻底终止,就变成TERMINATED状态。
    (02) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。


2.3 线程池的使用

2.3.1 创建线程池的方式 以及各参数含义

ExecutorService threadsPool  = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
milliseconds,runnableTaskQueue, handler);

其中:
corePoolSize(线程池的基本大小)
maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。
handle:当任务添加到线程池中被拒绝,而采取的处理措施

2.3.2 线程池的4种拒绝策略

线程池的拒绝策略,是指当任务添加到线程池中被拒绝,而采取的处理措施。
当任务添加到线程池中之所以被拒绝,可能是由于:第一,线程池异常关闭。第二,任务数量超过线程池的最大限制。

线程池共包括4种拒绝策略,它们分别是:AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy和DiscardPolicy。

  • AbortPolicy – 当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常。
  • CallerRunsPolicy – 当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。
  • DiscardOldestPolicy – 当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
  • DiscardPolicy – 当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。
    线程池默认的处理策略是AbortPolicy!

线程池的拒绝策略

2.3.3 线程池的4种阻塞队列

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原 则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通 常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工 厂方法Executors.newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

2.3.4 向线程池提交任务:execute() 和 submit()

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
  • submit()方法用于提交需要返回值的任务

线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池
executor.shutdown();
}

2.3.5 关闭线程池:shutdown()和 shutdownNow()

  可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
  shutdownNow首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线 程。
  只要调用了这两个关闭方法中的任意一个,isShutdown()方法就会返回true。当所有的任务 都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪 一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭 线程池,如果任务不一定要执行完,则可以调用shutdownNow方法
正确关闭线程池的方式


三.Executor 框架

Java的线程既是工作单元,也是执行机制。从JDK 5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。

Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程。

3.1 Executor 框架结构与执行流程

Executor 框架主要由三部分组成

  1. 任务:Runnable接口或Callable接口
  2. 任务的执行:包括任务执行机制的核心接口Executor(将任务的执行提交与执行分离开),以及继承自Executor的ExecutorService接口。
    Executor框架有两个关键类实现了ExecutorService接口
    • ThreadPoolExecutor(线程池,执行被提交的任务)
    • ScheduledThreadPoolExecutor(给定时间延迟后执行命令)
  3. 任务执行结果:接口Future和实现Future接口的FutureTask类

Java并发(10)-JUC线程池 Executor框架_第4张图片

整体执行流程:

  1. 主线程首先要创建实现Runnable或者Callable接口的任务对象。
  2. 然后可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnable command));
    或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(ExecutorService.submit(Runnable task)或ExecutorService.submit(Callabletask))。
  3. 如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象。
  4. 最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行
    FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

3.2 4种常用的ThreadPoolExecutor

ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建4种类型的线程池:

  • FixedThreadPool:适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器newFixedThreadPool(int nThreads)
// 创建一个可重用固定线程数的线程池
 ExecutorService pool = Executors.newFixedThreadPool(2);
  • SingleThreadExecutor:适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。
  • CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
  • ScheduledThreadPool,创建一个定长线程池,支持定时及周期性任务执行。

ThreadPoolExecutor数据结构


3.2.1 FixedThreadPool

FixedThreadPool被称为可重用固定线程数的线程池。

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。
1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中
的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。

Java并发(10)-JUC线程池 Executor框架_第5张图片

3.2.2 SingleThreadExecutor

SingleThreadExecutor是使用单个worker线程的Executor。

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, 
TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)


3.2.3 CachedThreadPool

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, 
TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}

CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的。这里把keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。
CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。


3.3 Executor 源码分析

  1. ThreadPoolExecutor类源码解析
  2. 创建线程池、提交任务、关闭线程池源码
  3. addWorker()详细分析

参考:
《java 并发编程的艺术》
Java多线程系列目录(共43篇)

你可能感兴趣的:(并发)