中北软院创新实验室

关于线程池的面试题

2018-09-19  本文已影响0人  HikariCP

问题

问题:

  1. 单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)
  2. 为什么要使用线程池,线程池用什么用
  3. 说说几种常见的线程池及使用场景
  4. 线程池有哪几种工作队列
  5. 怎么理解无界队列和有界队列
  6. 线程池中的几种重要的参数及流程

1. 为什么要使用线程池,线程池用什么用

帮我们合理的管理线程的生命周期,避免重复的创建及销毁大量的线程增加系统开销。

2. 说说几种常见的线程池及使用场景

newFixedThreadPool

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

线程池特点:

它是固定大小的线程池,其核心线程数和最大线程数大小一样。并且阻塞队列用的是LinkedBlockingQueue,也就是说线程最大数这个参数失效了基本,所以不会出现外包线程的存在,所以也可以认为keepAliveTime参数是一个摆设。除非allowCoreThreadTimeOut方法的调用。

该线程池的工作机制是:

  1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
  2. 线程数等于核心线程数后,将任务加入阻塞队列
    1. 由于队列容量非常大(Integer.MAX_VALUE),可以一直加加加。(当线程池中的任务比较特殊时,比如关于数据库的长时间的IO操作,可能导致OOM)
  3. 执行完任务的线程反复去队列中取任务执行

适用场景:

FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程即可。一般Ncpu+1

newSingleThreadExecutor

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

线程池特点:

该线程池的工作机制是:

  1. 线程池中没有线程时,新建一个线程执行任务
  2. 有一个线程以后,将任务加入阻塞队列,不停加加加
  3. 唯一的这一个线程不停地去队列里取任务执行

适用场景:

SingleThreadExecutor适用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

newCachedThreadPool

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

线程池特点:

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue

锁当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

该线程池的工作机制是:

  1. 没有核心线程,直接向SynchronousQueue中提交任务
  2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
  3. 执行完任务的线程有60秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜

适用场景:

CachedThreadPool 用于并发执行大量短期的小任务。

newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}


public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

线程池特点:

ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:

两种方法的内部实现都是创建了一个ScheduledFutureTask对象封装了任务的延迟执行时间及执行周期,并调用decorateTask()方法转成RunnableScheduledFuture对象,然后添加到延迟队列中。

DelayQueue:中封装了一个优先级队列,这个队列会对队列中的ScheduledFutureTask 进行排序,两个任务的执行 time 不同时,time 小的先执行;否则比较添加到队列中的ScheduledFutureTask的顺序号 sequenceNumber ,先提交的先执行。

该线程池的工作机制是:

  1. 调用上面两个方法添加一个任务
  2. 线程池中的线程从 DelayQueue 中取任务
  3. 然后执行任务

具体执行步骤:

  1. 线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask
    1. DelayQueue.take()
  2. 执行完后修改这个 task 的 time 为下次被执行的时间
  3. 然后再把这个 task 放回队列中
    1. DelayQueue.add()

适用场景:

ScheduledThreadPoolExecutor用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

3. 线程池有哪几种工作队列

4. 怎么理解无界队列和有界队列

5. 线程池中的几种重要的参数及流程

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;
}
image

执行流程

  1. 当有任务进入时,线程池创建线程去执行任务,直到核心线程数满为止
  2. 核心线程数量满了之后,任务就会进入一个缓冲的任务队列中
    1. 当任务队列为无界队列时,任务就会一直放入缓冲的任务队列中,不会和最大线程数量进行比较
    2. 当任务队列为有界队列时,任务先放入缓冲的任务队列中,当任务队列满了之后,才会将任务放入线程池,此时会拿当前线程数与线程池允许的最大线程数进行比较,如果超出了,则默认会抛出异常。如果没超出,然后线程池才会创建线程并执行任务,当任务执行完,又会将缓冲队列中的任务放入线程池中,然后重复此操作。

6. 单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)

经过网上查阅,发现基本是没有一个明确的回答的。不过思考过后一番,我感觉实现思路和MySQL的redo,undo功能很相似,我们可以对正在处理和阻塞队列的任务做事物管理或者对阻塞队列中的任务持久化处理,并且当断电或者系统崩溃,操作无法继续下去的时候,可以通过回溯日志的方式来撤销正在处理的已经执行成功的操作。然后重新执行整个阻塞队列。

即:

阻塞队列持久化,正在处理事物控制。断电之后正在处理的回滚,日志恢复该次操作。服务器重启后阻塞队列中的数据再加载

参考

上一篇下一篇

猜你喜欢

热点阅读