Java多线程~线程池

178 阅读13分钟

Java线程池作为Java多线程中更高级的知识点,也是面试中常被问到的知识点,下面带着几个常见的面试问题剖析下Java多线程中的线程池。

1. 经典面试题

  • 说说对Java线程池的理解,ThreadPoolExecutor各个参数的作用,如何进行的?
  • 几种常见的线程池及使用场景?
  • 线程池都有哪几种工作队列?
  • 使用无界队列的线程池会导致内存飙升吗?
  • 线程池中的线程抛异常会怎样?

2. 线程池的概念

  • 一句话简单概括:管理线程的池子
  • 主要优点:
    • 统一管理线程,避免线程的重复创建和销毁(线程也是一个类,而创建一个类对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的)
    • 重复利用。 线程使用直接从池子取,用完放回池子,可重复利用,节省资源。
    • 提高响应速度。  相对于从线程池拿线程,重新创建一个线程执行速度会慢很多。

3. 线程池的创建方式

线程池可以通过ThreadPoolExecutor类来创建,看下他参数最长的那个构造器

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

image.png 分别看下几个参数的意义

  • corePoolSize: 线程池核心线程数最大值
  • maximumPoolSize: 线程池中允许存放的最大线程数(核心线程数+非核心线程数)
  • keepAliveTime: 非核心线程池中空闲线程存活的时间
  • unit:  线程空闲存活时间单位,即keepAliveTime的时间单位,纳秒、毫秒、秒等
  • workQueue:  存放任务的阻塞队列,分为有界队列和无界队列,用于存放等待执行的任务,
  • threadFactory:  用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题,一般都是采用Executors.defaultThreadFactory()方法返回的DefaultThreadFactory
  • handler:  线程池的饱和策略(拒绝策略)事件,主要有四种类型(AbortPolicy、CallerRunsPolicy、 DiscardPolicy、DiscardOldestPolicy)

4. 线程池任务的执行流程

image.png

  • 通过execute()方法提交一个任务,当线程池里存活的核心线程数小于corePoolSize时,线程池会创建一个核心线程去处理提交的任务
  • 如果线程池中核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行
  • 当线程池里存活的线程数已经等于corePoolSize,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
  • 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用相应的拒绝策略进行处理
// 代码演示执行流程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20,
        10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),
        Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

for (int i = 1; i < 50; i++) {
    int finalI = i;
    threadPoolExecutor.execute(() -> {
        try {
            System.out.println("当前执行线程:" + finalI);
            Thread.sleep(1000);
            System.out.print("-------------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

执行结果
当前执行线程:1
当前执行线程:4
当前执行线程:3
当前执行线程:2
当前执行线程:6
当前执行线程:5
当前执行线程:7
当前执行线程:8
当前执行线程:9
当前执行线程:10
当前执行线程:21
当前执行线程:22
当前执行线程:26
当前执行线程:24
当前执行线程:23
当前执行线程:25
当前执行线程:27
当前执行线程:28
当前执行线程:29
当前执行线程:30
Exception in thread "main" java.util.concurrent.RejectedExecutionException....
-----------当前执行线程:12
-----当前执行线程:20
当前执行线程:19
当前执行线程:18
当前执行线程:17
当前执行线程:16
当前执行线程:15
当前执行线程:14
当前执行线程:13
-------------当前执行线程:11
  • 由代码演示可以看出,只有1-10和21-30先执行,中间的11-20过了1s后才执行,并且在执行到30之后会抛出RejectedExecutionException,这也验证了这一点,先创建1-10号核心线程,11-20加入到队列数最大为10的线程池阻塞队列,此时阻塞队列已满,21-30便加入非核心线程池,31之后的会执行拒绝策略(默认拒绝策略为抛异常),等到核心线程空闲便将阻塞队列中的任务开始执行

5. 线程池的工作队列

线程池的工作队列分为有界队列和无界队列,主要用于存放等待执行的任务

  • 有界队列:使用有界队列,添加新的任务进来时,如果线程池实际线程数小于corePoolSize(核心线程数),则优先创建核心线程,如果线程池实际线程数大于corePoolSize(核心线程数),则会将任务加入队列,若队列已满,则在线程数不大于maximumPoolSize(最大线程数)的前提下,创建新的线程,若线程数大于maximumPoolSize(最大线程数),则执行拒绝策略。常见的有界队列有:ArrayBlockingQueue
  • 无界队列:使用无界队列,maximumPoolSize(最大线程数)和拒绝策略均会失效,因为队列是没有限制的,所以就不存在队列满的情况。和有界队列相比,当有新的任务添加进来时,都会进入队列等待。但是这也会出现一些问题,例如线程的执行速度比任务提交速度慢,会导致无界队列快速增长,内存不断飙升,直到系统资源耗尽。常见的无界队列有:PriorityBlockingQueue

5.1 常见的线程池工作队列

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

6. 线程池的拒绝策略

  • AbortPolicy:抛出一个异常,默认的拒绝策略
  • DiscardPolicy:直接丢弃任务
  • DiscardOldestPolicy:丢弃队列里最老的任务,将当前这个任务继续提交给线程池
  • CallerRunsPolicy:交给线程池调用所在的线程进行处理
  • 自定义:可通过实现RejectedExecutionHandle接口来实现自定义拒绝策略

7. 常见线程池

  • newFixedThreadPool:固定数目线程的线程池
  • newCachedThreadPool:可缓存线程的线程池
  • newSingleThreadExecutor:单线程的线程池
  • newScheduledThreadPool:定时及周期执行的线程池

7.1 newFixedThreadPool

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

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
   return new ThreadPoolExecutor(nThreads, nThreads,
                                 0L, TimeUnit.MILLISECONDS,
                                 new LinkedBlockingQueue<Runnable>(),
                                 threadFactory);
}
  • 特点:
    • 核心线程数和最大线程数大小一样,可传参自定义
    • 没有所谓的非空闲时间,即keepAliveTime为0
    • 阻塞队列为无界队列LinkedBlockingQueue
  • 工作机制
    • 通过execute提交任务
    • 如果线程数少于核心线程,创建核心线程执行任务
    • 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
    • 如果线程执行完任务,去阻塞队列取任务,继续执行。
  • 实例
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    executorService.execute(() -> {
        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
        String currentTime= formatter.format(Calendar.getInstance().getTime());
        System.out.println(Thread.currentThread().getName() + "执行,时间为:" + currentTime);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

执行结果:
pool-1-thread-2执行,时间为:16:26:51
pool-1-thread-3执行,时间为:16:26:51
pool-1-thread-1执行,时间为:16:26:51
pool-1-thread-2执行,时间为:16:26:52
pool-1-thread-3执行,时间为:16:26:52
pool-1-thread-1执行,时间为:16:26:52
pool-1-thread-2执行,时间为:16:26:53
pool-1-thread-1执行,时间为:16:26:53
pool-1-thread-3执行,时间为:16:26:53
pool-1-thread-1执行,时间为:16:26:54
  • 使用场景:处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能少的分配线程,即适用执行长期的任务(由于使用了无界队列,当任务执行耗时长无法释放,而不断新增任务时会有内存飙升甚至OOM的风险

7.2 newCachedThreadPool

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

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}
  • 特点:
    • 核心线程数可传参自定义
    • 最大线程数为Integer.MAX_VALUE
    • 阻塞队列是SynchronousQueue
    • 非核心线程空闲存活时间为60秒
  • 工作机制:
    • 通过execute提交任务
    • 由于没有核心线程,所以任务会直接加到SynchronousQueue队列。
    • 判断是否有空闲线程,如果有,就去取出任务执行。
    • 如果没有空闲线程,就新建一个线程执行。
    • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。
  • 实例:
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    executorService.execute(() -> {
        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
        String currentTime= formatter.format(Calendar.getInstance().getTime());
        System.out.println(Thread.currentThread().getName() + "执行,时间为:" + currentTime);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

执行结果:
pool-1-thread-9执行,时间为:16:25:55
pool-1-thread-2执行,时间为:16:25:55
pool-1-thread-3执行,时间为:16:25:55
pool-1-thread-8执行,时间为:16:25:55
pool-1-thread-5执行,时间为:16:25:55
pool-1-thread-7执行,时间为:16:25:55
pool-1-thread-6执行,时间为:16:25:55
pool-1-thread-4执行,时间为:16:25:55
pool-1-thread-10执行,时间为:16:25:55
pool-1-thread-1执行,时间为:16:25:55
  • 使用场景:用于并发执行大量短期的小任务

7.3 newSingleThreadExecutor

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

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}
  • 特点:
    • 核心线程数与最大线程数均为1
    • 最大线程数为Integer.MAX_VALUE
    • 阻塞队列是LinkedBlockingQueue
  • 工作机制:
    • 通过execute提交任务
    • 线程池是否有一个线程,如果没有,新建线程执行任务
    • 如果有,将任务加到LinkedBlockingQueue阻塞队列
    • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。
  • 实例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
    executorService.execute(() -> {
        SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
        String currentTime= formatter.format(Calendar.getInstance().getTime());
        System.out.println(Thread.currentThread().getName() + "执行,时间为:" + currentTime);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

执行结果:
pool-1-thread-1执行,时间为:16:24:40
pool-1-thread-1执行,时间为:16:24:41
pool-1-thread-1执行,时间为:16:24:42
pool-1-thread-1执行,时间为:16:24:43
pool-1-thread-1执行,时间为:16:24:44
pool-1-thread-1执行,时间为:16:24:45
pool-1-thread-1执行,时间为:16:24:46
pool-1-thread-1执行,时间为:16:24:47
pool-1-thread-1执行,时间为:16:24:48
pool-1-thread-1执行,时间为:16:24:49
  • 使用场景:任务串行执行,一个任务一个任务地顺序执行

7.4 newScheduledThreadPool

  • 源码:
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory);
}
  • 特点:
    • 核心线程数可传参自定义
    • 最大线程数为Integer.MAX_VALUE
    • keepAliveTime为0
    • 阻塞队列为延迟队列DelayedWorkQueue
    • 方法scheduleAtFixedRate()可让任务按某种速率周期执行
    • 方法scheduleWithFixedDelay()可让任务在延迟后执行
  • 工作机制:
    • 通过execute提交任务
    • 线程池中的线程从延迟队列DelayQueue中取任务
    • 线程从 DelayQueue 中获取 time 大于等于当前时间的任务
    • 执行完后修改这个任务task的 time 为下次被执行的时间
    • 这个任务放回DelayQueue队列中
  • 实例:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleWithFixedDelay(() -> {
    SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
    String currentTime = formatter.format(Calendar.getInstance().getTime());
    System.out.println(Thread.currentThread().getName() + "执行,时间为:" + currentTime);
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, 1, 3, TimeUnit.SECONDS);

执行结果:
pool-1-thread-1执行,时间为:17:34:42
pool-1-thread-1执行,时间为:17:34:46
pool-1-thread-1执行,时间为:17:34:50
pool-1-thread-1执行,时间为:17:34:54
pool-1-thread-1执行,时间为:17:34:58
pool-1-thread-1执行,时间为:17:35:02
pool-1-thread-1执行,时间为:17:35:06
...
  • 使用场景:任务需要周期性的执行,需要限制线程数量

8. 线程池的运行状态

跟线程一样,线程池也存在自己的运行状态

// ThreadPoolExecutor.java
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

image.png

  • RUNNING
    • 线程池会同时处理阻塞任务和接新任务
    • 调用shutdown()进入SHUTDOWN状态、调用shutdownNow()进入STOP状态
  • SHUTDOWN
    • 线程池只会处理阻塞队列中的任务,不再接新任务
    • 当处理完所有任务,此时任务队列也空了,会进入TIDYING状态
  • STOP
    • 线程池既不会接新任务,也不会处理阻塞队列中的任务,并且还会中断正在运行的任务
    • 当任务队列为空会进入TIDYING状态
  • TIDYING
    • 线程池中所有的任务已经运行终止,记录的任务数量为0。
    • 当terminated()执行完毕,标志着进入TERMINATED状态
  • TERMINATED
    • 线程池彻底终止

9. 线程池中的异常处理方式

线程池处理任务时,线程中可能会抛出RuntimeException异常,当然线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,而我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

public static void main(String[] args) {
 ExecutorService executorService = Executors.newFixedThreadPool(3);
 for (int i = 0; i < 5; i++) {
     executorService.submit(() -> {
         System.out.println("当前执行线程" + Thread.currentThread().getName());
         Object object = null;
         object.toString();
     });
 }
}

执行结果:
当前执行线程pool-1-thread-2
当前执行线程pool-1-thread-1
当前执行线程pool-1-thread-3
当前执行线程pool-1-thread-1
当前执行线程pool-1-thread-2

可见并未抛出异常的信息,那么如何感知异常处理异常呢,常见有以下几种方法

9.1 try-catch

ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
    executorService.submit(() -> {
        Object object = null;
        try {
            object.toString();
        } catch (Exception e) {
           System.out.println("当前执行线程:" + Thread.currentThread().getName() + " 抛出异常:" + e);
        }
    });
}

执行结果:
当前执行线程:pool-1-thread-2 抛出异常:java.lang.NullPointerException
当前执行线程:pool-1-thread-3 抛出异常:java.lang.NullPointerException
当前执行线程:pool-1-thread-1 抛出异常:java.lang.NullPointerException
当前执行线程:pool-1-thread-2 抛出异常:java.lang.NullPointerException
当前执行线程:pool-1-thread-3 抛出异常:java.lang.NullPointerException

9.2 Future.get()

ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
    Future<?> future = executorService.submit(() -> {
        System.out.print("当前执行线程" + Thread.currentThread().getName());
        Object object = null;
        object.toString();
    });

    try {
        future.get();
    } catch (Exception e) {
        System.out.println(",抛出异常:" + e);
    }
}

执行结果:
当前执行线程pool-1-thread-1,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
当前执行线程pool-1-thread-2,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
当前执行线程pool-1-thread-3,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
当前执行线程pool-1-thread-1,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
当前执行线程pool-1-thread-2,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException

9.3 UncaughtExceptionHandler

可以通过线程设置UncaughtExceptionHandler来达到监听的目的

ExecutorService executorService = Executors.newFixedThreadPool(3, thread -> {
    Thread thread1 = new Thread(thread);
    thread1.setUncaughtExceptionHandler((t1, e) -> {
        System.out.println("当前执行线程" + t1.getName() + "抛出异常:" + e);
    });
    return thread1;
});

for (int i = 0; i < 5; i++) {
    executorService.execute(() -> {
        Object object = null;
        object.toString();
    });
}

执行结果:
当前执行线程Thread-2抛出异常:java.lang.NullPointerException
当前执行线程Thread-4抛出异常:java.lang.NullPointerException
当前执行线程Thread-0抛出异常:java.lang.NullPointerException
当前执行线程Thread-1抛出异常:java.lang.NullPointerException
当前执行线程Thread-5抛出异常:java.lang.NullPointerException