0%

面试题-线程池

本章属于持续学习、长期更修。

为避免线程频繁创建和销毁带来的性能问题,而池化的一种方案。

  • 利用线程池管理并复用线程、控制最大并发数等。

  • 实现任务线程队列缓存策略和拒绝机制。

  • 实现某些与时间相关的功能,如定时执行、周期执行等。

  • 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池将较慢的交易服务与搜索服务隔离开,避免各服务线程相互影响。

这里直接参考阿里巴巴的手册。线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。

常⽤线程池:ExecutorService 是主要的实现类

  • Executors.newSingleT hreadPool()
  • newFixedThreadPool()
  • newcachedTheadPool()
  • newScheduledThreadPool()

Executors 返回的线程池对象的弊端

  • FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
  • 查看 Executors 的源码会发现,Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor() 和 Executors.newCachedThreadPool() 等方法的底层都是通过 ThreadPoolExecutor 实现的。

ThreadPoolExecutor的核心参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        // maximumPoolSize 必须大于 0,且必须大于 corePoolSize
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • 第 1 个参数:corePoolSize 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程(创建和销毁的原因会在本课时的下半部分讲到);如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值。
  • 第 2 个参数:maximumPoolSize 表示线程池在任务最多时,最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize,此值只有在任务比较多,且不能存放在任务队列时,才会用到。
  • 第 3 个参数:keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于 corePoolSize 为止,如果 maximumPoolSize 等于 corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。
  • 第 4 个参数:unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的。
  • 第 5 个参数:workQueue 表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。
  • 第 6 个参数:threadFactory 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程。
  • 第 7 个参数:RejectedExecutionHandler 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列 workQueue 中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。
  • 原理
    • 如果当前池⼤⼩ poolSize ⼩于 corePoolSize,则创建新线程执⾏任务。
    • 如果当前池⼤⼩poolSize⼤于corePoolSize,且等待队列未满,则进⼊等待队列。
    • 如果当前池⼤⼩ poolSize ⼤于 corePoolSize 且⼩于 maximumPoolSize ,且等待队列已满,则创建新线程执⾏任务。
    • 如果当前池⼤⼩ poolSize ⼤于 corePoolSize 且⼤于 maximumPoolSize ,且等待队列已满,则调⽤拒绝策略来处理该任务。
      • 线程池⾥的每个线程执⾏完任务后不会⽴刻退出,⽽是会去检查下等待队列⾥是否还有线程任务需要执⾏,如果在 keepAliveTime ⾥等不到新的任务了,那么线程就会退出。
  • ThreadPoolExecutor 3 个最重要的参数:
    • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
    • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
    • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

线程池任务执行的主要流程

execute() VS submit()

execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ThreadPoolExecutor executor = new ThreadPoolExecutor(21010L,
        TimeUnit.SECONDS, new LinkedBlockingQueue(20));
// execute 使用
executor.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, execute.");
    }
});
// submit 使用
Future<String> future = executor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        System.out.println("Hello, submit.");
        return "Success";
    }
});
System.out.println(future.get());

// 执行结果
//Hello, submit.
//Hello, execute.
//Success

线程池的拒绝策略

当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。

  • Java 自带的拒绝策略有 4 种:

  • AbortPolicy,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略。

  • CallerRunsPolicy,把任务交给当前线程来执行。

  • DiscardPolicy,忽略此任务(最新的任务)。

  • DiscardOldestPolicy,忽略最早的任务(最先加入队列的任务)。

  • 演示AbortPolicy

1
2
3
4
5
6
7
8
ThreadPoolExecutor executor = new ThreadPoolExecutor(1310,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
        new ThreadPoolExecutor.AbortPolicy()); // 添加 AbortPolicy 拒绝策略
for (int i = 0; i < 6; i++) {
    executor.execute(() -> {
        System.out.println(Thread.currentThread().getName());
    });
}

结果

1
2
3
4
5
6
7
8
9
10
11
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.lagou.interview.ThreadPoolExample$$Lambda$1/1096979270@448139f0 rejected from java.util.concurrent.ThreadPoolExecutor@7cca494b[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]
 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
 at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
 at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
 at com.lagou.interview.ThreadPoolExample.rejected(ThreadPoolExample.java:35)
 at com.lagou.interview.ThreadPoolExample.main(ThreadPoolExample.java:26)
  • 第 6 个任务来的时候,线程池则执行了 AbortPolicy 拒绝策略,抛出了异常。因为队列最多存储 2 个任务,最大可以创建 3 个线程来执行任务(2+3=5),所以当第 6 个任务来的时候,此线程池就“忙”不过来了。

Java⾥的阻塞队列

  • 7个队列阻塞
    • ArrayBlockingQueue :⼀个由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue :⼀个由链表结构组成的有界阻塞队列。
    • PriorityBlockingQueue :⼀个⽀持优先级排序的⽆界阻塞队列。
    • DelayQueue:⼀个使⽤优先级队列实现的⽆界阻塞队列。
    • SynchronousQueue:⼀个不存储元素的阻塞队列。
    • LinkedTransferQueue:⼀个由链表结构组成的⽆界阻塞队列。
    • LinkedBlockingDeque:⼀个由链表结构组成的双向阻塞队列。

如果你提交任务时,线程池队列已满。会时发会⽣什么?

  • ⼀个任务不能被调度执⾏那么ThreadPoolExecutor’s submit()⽅法将会抛出⼀个RejectedExecutionException异常。

shutdown()shutdownNow()

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

isTerminated()isShutdown()

  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

如何合理的配置java线程池?

  • 如CPU密集型的任务,基本线程池应该配置多⼤?IO密集型的任务,基本线程池应该配置多⼤?⽤有界队列好还是⽆界队列好?任务⾮常多的时候,使⽤什么阻塞队列能获取最好的吞吐量?

    • 配置线程池时CPU密集型任务可以少配置线程数,⼤概和机器的cpu核数相当,可以使得每个线程都在执⾏任务。
    • IO密集型时,⼤部分线程都阻塞,故需要多配置线程数,2*cpu核数
    • 有界队列和⽆界队列的配置需区分业务场景,⼀般情况下配置有界队列,在⼀些可能会有爆发性增⻓的情况下使⽤⽆界队列。
    • 任务⾮常多时,使⽤⾮阻塞队列使⽤CAS操作替代锁可以获得好的吞吐量。
    • 有一个简单并且适用面比较广的公式:
      • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
      • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
    • 如何判断是 CPU 密集任务还是 IO 密集任务?
      • CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。