备战Java面试[多线程并发] -- 线程池详细讲解

342 阅读9分钟

请添加图片描述请添加图片描述

线程池

池化技术

程序的运行,本质:占用系统的资源!,我们需要去优化资源的使用,于是有了 池化技术 例如: 线程池、JDBC的连接池、内存池、对象池 等等 资源的创建、销毁十分消耗资源 池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

为什么要使用线程池?

Java的线程池是运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。 合理使用线程池能带来的好处:

  • 降低资源消耗。 通过重复利用已经创建的线程降低线程创建的和销毁造成的消耗。例如,工作线程Woker会无线循环获取阻塞队列中的任务来执行。
  • 提高响应速度。 当任务到达时,任务可以不需要等到线程创建就能立即执行。 提高线程的可管理性。
  • 线程是稀缺资源,Java的线程池可以对线程资源进行统一分配、调优和监控

1、三大方法

//工具类 Executors 三大方法;
public class Demo01 {
    public static void main(String[] args) {
 
        ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
        ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
        ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
 
        //线程池用完必须要关闭线程池
        try {
 
            for (int i = 1; i <=100 ; i++) {
                //通过线程池创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+ " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

源码分析

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

阿里巴巴Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池在这里插入图片描述

2、七大参数

在这里插入图片描述

public ThreadPoolExecutor(int corePoolSize,  //核心线程池大小
                          int maximumPoolSize, //最大的线程池大小
                          long keepAliveTime,  //超时了没有人调用就会释放
                          TimeUnit unit, //超时单位
                          BlockingQueue<Runnable> workQueue, //阻塞队列
                          ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
                          RejectedExecutionHandler handler //拒绝策略
                         ) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

详细说明: 1. corePoolSize(线程池的基本大小)

  • 提交一个任务到线程池时,线程池会创建一个新的线程来执行任务。注意: 即使有空闲的基本线程能执行该任务,也会创建新的线程

  • 如果线程池中的线程数已经大于或等于corePoolSize,则不会创建新的线程。 如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

2.maximumPoolSize(线程池的最大数量):

  • 线程池允许创建的最大线程数。
  • 阻塞队列已满,线程数小于maximumPoolSize便可以创建新的线程执行任务
  • 如果使用无界的阻塞队列,该参数没有什么效果。

3. workQueue(工作队列)

  • 用于保存等待执行的任务的阻塞队列。

4.keepAliveTime(线程活动保持时间)

  • 线程池的工作线程空闲后,保持存活的时间。如果任务多而且任务的执行时间比较短,可以调大keepAliveTime,提高线程的利用率。

5.unit(线程活动保持时间的单位)

  • 可选单位有DAYS、HOURS、MINUTES、毫秒、微秒、纳秒。

6.handler(饱和策略,或者又称拒绝策略): 当队列和线程池都满了,即线程池饱和了,必须采取一种策略处理提交的新任务。

  • AbortPolicy: 无法处理新任务时,直接抛出RejectedExecutionException异常,这是默认策略。
  • CallerRunsPolicy:用调用者所在的线程来执行任务, 会导致主线程阻塞。
  • DiscardOldestPolicy:丢弃阻塞队列中最靠前的一个任务,并执行当前任务。
  • DiscardPolicy: 直接丢弃任务。

7. threadFactory: 构建线程的工厂类

3、线程池的五种状态

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

线程池各个状态切换框架图: 在这里插入图片描述 1.RUNNING

  • 状态说明: 线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。

  • 状态切换: 线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0

2.SHUTDOWN

  • 状态说明: 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务

  • 状态切换: 调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3.STOP

  • 状态说明: 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务

  • 状态切换: 调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4.TIDYING

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

5.TERMINATED

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

4、线程池工作流程

一个新的任务到线程池时,线程池的处理流程如下: 在这里插入图片描述

ThreadPoolExecutor类具体的处理流程:

线程池的核心实现类是ThreadPoolExecutor类,用来执行提交的任务。因此,任务提交到线程池时,具体的处理流程是由ThreadPoolExecutor类的execute()方法去完成的。 在这里插入图片描述

  1. 创建了线程池后,等待提交过来的任务请求
  2. 当调用execute()方法添加一个请求任务时,线程池会做如下判断: 2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; 2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列; 2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; 2.4如果队列满了正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  3. 一个线程完成任务时,它会从队列中取下一个任务来执行
  4. 一个线程无事可做超过一定的时间(keepAliveTime) 时,线程池会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小

以银行业务来类比我们线程池

在这里插入图片描述 具体业务流程:

  1. 核心线程数(corePoolSize)(对应我们当值的窗口), 一般请求比较少的时候只有核心线程开着,
  2. 核心线程都已被占用了(当值窗口都有人了), 这时新的请求进来, 于是进入我们的阻塞队列(候客区)
  3. 当请求过多, 阻塞队列也满了 (候客区满是等待的人,开始发牢骚了) , 于是经理打电话叫休假的人来加班, 也就是开启我们其他的窗口, 此时线程开到最大线程数(maximumPoolSize)
  4. 请求继续增多, 以至于数量超过了最大线程数+ 阻塞队列(所有窗口都有人并且候客区也满了), 这时候经理要在门口维持秩序, 阻止新的客人进来, 也就是启动了拒绝策略
  5. 当客人们办完事, 陆续开始离场, 于是加班的窗口开始空闲, 但是他们并不会立马走人,而是打起了王者荣耀,看看会不会有多的人过来办理, 当他们打完一局后, 发现没有人来了, 于是准备回家睡觉, 这就是空余线程的存活时间(keepAliveTime), 只有当线程数大于核心线程, 空闲时间超过keepAliveTime, 多余空闲线程会销毁, 直到剩下核心线程数

5.阻塞队列

①特点:

  • 当阻塞队列是时,从队列中获取元素的操作将会被阻塞
  • 当阻塞队列是时,往队列里添加元素的操作将会被阻塞

②分类:

ArrayBlockingQueue:数组结构组成的有界限塞队列。.

LinkedBlockingQueue:链表结构组成的有界(但大小默认值为Integer.MAX VALUG)阻塞队列(几乎可以认为是无界)

③PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

④DelayQueue:使用优先级队列实现的延迟(按延迟时间排序)无界阻塞队列。

SynchronousQueue: 不存储元素的阻塞队列,也即单个元素的队列

⑥LinkedTransferQueue:由链表结构组成的无界阻塞队列。

⑦LinkedBlockingDeque:由链表结构组成的双向阻塞队列

③BlockQueue核心方法:

在这里插入图片描述

6、四种拒绝策略源码解析

RejectedExecutionHandler接口

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
  • 里面只有一个方法。当要创建的线程数量大于线程池的最大线程数的时候,新的任务就会被拒绝,就会调用这个接口里的这个方法。

  • 可以自己实现这个接口,实现对这些超出数量的任务的处理。

ThreadPoolExecutor自己已经提供了四个拒绝策略,分别是CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy

这四个拒绝策略其实一看实现方法就知道很简单。

①AbortPolicy

ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常。

private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

实现:

public static class AbortPolicy implements RejectedExecutionHandler {
    public AbortPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

②CallerRunsPolicy

"调用者运行",该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者去执行 实现:

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public CallerRunsPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

③DiscardPolicy

这个策略的处理就更简单了,看一下实现就明白了:

public static class DiscardPolicy implements RejectedExecutionHandler {
    public DiscardPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}

这个东西什么都没干,因此采用这个拒绝策略,会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行。

④DiscardOldestPolicy

DiscardOldestPolicy策略的作用是,当任务拒绝添加时,会抛弃任务队列中最旧的任务(也就是最先加入队列的任务),再把这个新任务添加进去。

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public DiscardOldestPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}

在rejectedExecution先从任务队列总弹出最先加入的任务,空出一个位置,然后再次执行execute方法把任务加入队列。

⑤自定义拒绝策略

通过看前面的系统提供的四种拒绝策略可以看出,拒绝策略的实现都非常简单。自己写亦一样

比如现在想让被拒绝的任务在一个新的线程中执行,可以这样写:

static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        new Thread(r,"新线程"+new Random().nextInt(10)).start();
    }
}

7.如何合理配置线程数

①CPU密集型

  • CPU密集的意思是该任务需要大量的运算,而没有阻塞CPU一直全速运行
  • CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上(悲剧吧),无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量: 一般公式: CPU核数+1个线程的线程池

②IO密集型

第一种: 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2

第二种:IO密集型,即该任务需要大量的IO,即大量的阻塞。

单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力。 所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数: 参考公式: CPU核数 / 1 - 阻塞系数 阻塞系数在0.8~0.9之间

比如8核CPU:

  • 8 / 1 - 0.9 = 80个线程数

请添加图片描述请添加图片描述

参考文章: java多线程-ThreadPoolExecutor的拒绝策略 深入理解线程池底层原理