面试官:看你简历上写着熟练掌握线程池?

190 阅读9分钟

1 什么是线程池

简单的说线程池就是管理线程的池子,避免我们在使用线程的过程中不断的创建大量的线程增加开销,提高响应速度。

1.1 线程池的优势

  • 管理线程的创建和销毁:
    可以把线程也看成一个对象。一个对象的创建、加载、初始化、和销毁都是需要资源的开销的。
  • 提高效应速度:
    任务到达直接从池中那获取线程和每次去创建一个线程,两者肯定是有明显区别的。
  • 重复利用:
    使用完后,再将线程放回池子,可以重复的利用。

2 创建线程池

2.1 线程池ThreadPoolExecutor构造函数

五个参数的构造函数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)

六个参数的构造函数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) 

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)

七个参数的构造函数

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 

看起来是不是有点可怕,下面我们来一个个的分析每个参数的作用。

  • corePoolSize: 该线程池中核心线程数最大值
    核心线程数就是在创建线程池之后,核心线程先不创建,在接到任务后创建核心线程。且核心线程会一直存在于线程池中,即使没有任务要执行,也不会对线程进行销毁。
package com.example.demo.test.threadPool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorTest {
        private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                6464,
                0,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<>(32)
        );

    public static void main(String[] args) {
        
        System.out.println("进入主方法,线程数="+threadPoolExecutor.getPoolSize());
        for (int i = 0; i < 1; i++) {
            threadPoolExecutor.execute(() ->{
                System.out.println("线程开始执行任务了!");
                System.out.println("线程数="+threadPoolExecutor.getPoolSize());
            });
        }
    }
}
//    打印结果
//    进入主方法,线程数=0
//    线程开始执行任务了!
//    线程数=1

我们可以看到,我们创建完线程池之后并不会立即就创建线程,而是等到需要执行任务的时候才会优先创建核心线程。

  • maximumPoolSize: 程池最大线程数大小
    线程总数=核心线程数 + 非核心线程数
    非核心线程数简单的说就是我们创建的核心线程数不够用了,但是还有任务在一直请求执行,这时候就需要创建非核心线程。
  • keepAliveTime:非核心线程闲置超时时长
    这个参数可以理解为,任务少,但池中线程多,非核心线程不能白养着,超过这个时间不工作的就会被干掉,但是核心线程会保留。
  • TimeUnit:非核心线程闲置超时时长的单位
  • BlockingQueue workQueue -> 线程池中的任务队列
    默认情况下,任务进来之后先分配给核心线程执行,核心线程如果都被占用,并不会立刻开启非核心线程执行任务,而是将任务插入任务队列等待执行,核心线程会从任务队列取任务来执行,任务队列可以设置最大值,一旦插入的任务足够多,达到最大值,才会创建非核心线程执行任务。
  • ThreadFactory threadFactory
    可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置该参数。
  • RejectedExecutionHandler:饱和策略(拒绝策略)

2.2 常见的线程池任务队列

  • SynchronousQueue(同步队列):
    阻塞队列,可能称之为队列不太准确,因为它内部没有数据存储的空间,任何入队的操作都会阻塞,直到有线程出列,也就是这个队列是一组操作,入队和出队要一起操作。
    我们举个例子帮助我们理解:
public class SynchronousQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 创建SynchronousQueue队列
        BlockingQueue<Integer> synchronousQueue = new SynchronousQueue<>();
 
        // 2. 启动一个线程,往队列中放3个元素
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " 入队列 1");
                synchronousQueue.put(1);
                Thread.sleep(1);
                System.out.println(Thread.currentThread().getName() + " 入队列 2");
                synchronousQueue.put(2);
                Thread.sleep(1);
                System.out.println(Thread.currentThread().getName() + " 入队列 3");
                synchronousQueue.put(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
 
        // 3. 等待1000毫秒
        Thread.sleep(1000L);
 
        // 4. 再启动一个线程,从队列中取出3个元素
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " 出队列 " + synchronousQueue.take());
                Thread.sleep(1);
                System.out.println(Thread.currentThread().getName() + " 出队列 " + synchronousQueue.take());
                Thread.sleep(1);
                System.out.println(Thread.currentThread().getName() + " 出队列 " + synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
 
}
#执行结果
#Thread-0 入队列 1
#Thread-1 出队列 1
#Thread-0 入队列 2
#Thread-1 出队列 2
#Thread-0 入队列 3
#Thread-1 出队列 3

从输出结果中可以看到,第一个线程Thread-0往队列放入一个元素1后,就被阻塞了。直到第二个线程Thread-1从队列中取走元素1后,Thread-0才能继续放入第二个元素2。
基于这种特性,因为每次有任务进来,就直接回创建线程去处理了,我们通常把为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大。但是也会存在一个隐患,那就是一直有任务进来,就会一直创建线程。

  • LinkedBlockingQueue(可设置容量队列):
    基于链表结构的阻塞队列,按FIFO(先进先出)排序任务,这个队列接收到任务的时候,如果当前已经创建的核心线程数小于线程池的核心线程数上限,则新建线程(核心线程)处理任务;如果当前已经创建的核心线程数等于核心线程数上限,则进入队列等待。容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,此时所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize。
  • ArrayBlockingQueue(有界队列):
    一个用数组实现的有界阻塞队列。接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误,或是执行实现定义好的饱和策略。
  • DelayQueue(延迟队列):
    队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。
  • PriorityBlockingQueue(优先级队列):
    具有优先级的无界阻塞队列。

2.3 常见的拒绝策略

  • AbortPolicy:
    默认的拒绝策略,当任务队列和线程池都满了。无法处理新任务,并抛出 RejectedExecutionException 异常。
  • CallerRunsPolicy
    用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
  • DiscardPolicy
    不能执行的任务,并将该任务删除。
  • DiscardOldestPolicy
    丢弃队列最近的任务,并执行当前的任务。

3 线程池执行流程

了解了线程池的基本概念和关键参数,下面看下线程池完整的执行流程。 线程池执行流程

4 常用到的线程池

除了通过new ThreadPoolExecutor创建线程池,我们也可以根据各自的需求场景创建不同的线程池,下面介绍几种常见的线程池。

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

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

4.1.1 线程池的特点

  • 核心线程数和最大线程数大小一样。
  • 非核心线程闲置超时时长为0,他也不存在非核心线程数。
  • 阻塞队列为无界队列LinkedBlockingQueue

4.1.2 工作机制

  • 提交任务。
  • 如果线程数小于核心线程数,创建核心线程执行任务。
  • 如果线程数大于核心线程数,任务进入无界阻塞队列。

4.1.3 弊端

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

4.2 newCachedThreadPool(可缓存线程池):

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

4.2.1 线程池的特点

  • 核心线程数为0。
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒。因此长时间保持空闲的newCachedThreadPool不会使用任何资源

4.2.2 工作机制

  • 提交任务。
  • 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

4.2.3 弊端

CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务速度高于maximumPool中线程处理任务速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。这个限制了该线程池适合用于并发执行大量短期的小任务。

4.3 newSingleThreadExecutor(单线程线程池):

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

4.3.1 线程池的特点

  • 核心线程数为1。
  • 最大线程数也为1
  • 阻塞队列是LinkedBlockingQueue
  • 非核心线程空闲存活时间为0秒。

4.3.2 工作机制

  • 提交任务。
  • 线程池是否有一条线程在,如果没有,新建线程执行任务。
  • 如果有,将任务加到阻塞队列。
  • 如果没有空闲线程,就新建一个线程执行。
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取。适用于串行执行任务的场景,一个任务一个任务地执行。

好啦!今天比昨天进步一点,就是前行路上最大的幸运。