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

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

线程池

线程池:三大方法、7大参数、4种拒绝策略

池化技术

程序的运行,本质:占用系统的资源!
我们需要去优化资源的使用 ===> 池化技术
例如:线程池、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来创建线程池
备战Java面试[JUC并发编程] -- 线程池详细讲解

2、七大参数

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

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()方法,线程池会提前创建并启动所有基本线程。
    maximumPoolSize(线程池的最大数量): 线程池允许创建的最大线程数。

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

2. workQueue(工作队列): 用于保存等待执行的任务的阻塞队列。

  • ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO(先进先出)原则对任务进行排序。使用该队列,线程池中能创建的最大线程数为maximumPoolSize。
  • LinkedBlockingQueue: 基于链表结构的*阻塞队列,按FIFO(先进先出)原则对任务进行排序,吞吐量高于ArrayBlockingQueue。使用该队列,线程池中能创建的最大线程数为corePoolSize。静态工厂方法 Executor.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue: 一个不存储元素的阻塞队列。添加任务的操作必须等到另一个线程的移除操作,否则添加操作一直处于阻塞状态。静态工厂方法 Executor.newCachedThreadPool()使用了这个队列。
  • PriorityBlokingQueue: 一个支持优先级的*阻塞队列。使用该队列,线程池中能创建的最大线程数为corePoolSize。

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

5.unit(线程活动保持时间的单位): 可选单位有DAYS、HOURS、MINUTES、毫秒、微秒、纳秒。

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

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

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

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

备战Java面试[JUC并发编程] -- 线程池详细讲解
具体流程:

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

3、线程池工作流程

一个新的任务到线程池时,线程池的处理流程如下:
备战Java面试[JUC并发编程] -- 线程池详细讲解

ThreadPoolExecutor类具体的处理流程:

线程池的核心实现类是ThreadPoolExecutor类,用来执行提交的任务。因此,任务提交到线程池时,具体的处理流程是由ThreadPoolExecutor类的execute()方法去完成的。
备战Java面试[JUC并发编程] -- 线程池详细讲解

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

4、四种拒绝策略

上一篇:大厂学院JUC并发编程与源码分析


下一篇:【转】如何在Windows+VS2005使用最新静态libcurl 7.35.0获取网页数据,支持HTTPS