连线程池都不懂的程序员,还张口闭口高并发?

在这里插入图片描述

前言:

线程池是面试常考知识点,比如:如何创建线程池、线程池有什么参数及参数的用意、有哪些拒绝策略、线程池原理、如何监控等等,本篇文章一一为您解答,祝各位顺利找到满意的高薪工作。最近因为面试也有整理一些面试资料&最新2020收集的一些大厂的面试真题(都整理成文档,小部分截图),有需要的可以点击进入领取 暗号:csdn

在这里插入图片描述

一、简介

什么是线程池?

池的概念大家也许都有所听闻,池就是相当于一个容器,里面有许许多多的东西你可以即拿即用。java中有线程池、连接池等等。线程池就是在系统启动或者实例化池时创建一些空闲的线程,等待工作调度,执行完任务后,线程并不会立即被销毁,而是重新处于空闲状态,等待下一次调度。

线程池的工作机制?

在线程池的编程模式中,任务提交并不是直接提交给线程,而是提交给池。线程池在拿到任务之后,就会寻找有没有空闲的线程,有则分配给空闲线程执行,暂时没有则会进入等待队列,继续等待空闲线程。如果超出最大接受的工作数量,则会触发线程池的拒绝策略。

为什么使用线程池?

线程的创建与销毁需要消耗大量资源,重复的创建与销毁明显不必要。而且池的好处就是响应快,需要的时候自取,就不会存在等待创建的时间。线程池可以很好地管理系统内部的线程,如数量以及调度。

二、线程池的使用

在这里插入图片描述

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

1、关键参数

扫描二维码关注公众号,回复: 11596854 查看本文章
  • corePoolSize 核心线程数

当向线程池中提交一个任务时,如果线程池中的线程数量小于核心线程数,即使存在空闲线程,也会新建一个线程来执行当前任务,直到线程数量大于或等于核心线程数。

  • maximunPoolSize 最大线程数

当任务队列满了,线程池中的线程数量小于最大线程数时,创建新线程执行任务。对于无界队列,忽略该参数。

  • keepAliveTime 线程存活时间

大于核心线程数的那一部分线程的存活时间,如果这部分线程空闲超过这段时间,则进行销毁。

  • workqueue 任务队列

线程池中的线程数大于核心线程数时,将任务放入此队列等待执行。

  • threadFactory 线程工厂

用于创建线程,工厂使用 new Threa() 的方式创建线程,并为每个线程做统一规则的命名:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

  • handler 饱和策略

当线程池和队列都满了,则根据此策略处理任务。

2、任务队列类型
在这里插入图片描述
3、饱和策略类型

在这里插入图片描述
同时,还可以自行实现 RejectedExecutionHandler 接口来自定义饱和策略,比如记录日志、持久化等等。
void execute(Runnable command)

ThreadFactory namedThreadFactory =
new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
ExecutorService executor =new ThreadPoolExecutor(
10,  
1000,
60L,
TimeUnit.SECONDS,new LinkedBlockingQueue<>(10),
namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
executor.execute(() -> {
System.out.println(1111);
});

注意使用 execute 方法提交任务时,没有返回值。

Future<?> submit(Runnable task)

Future<Integer> future = executor.submit(() -> {
   return 1 + 1;
  });Integer result = future.get();

还可以使用 submit 方法提交任务,该方法返回一个 Future 对象,通过 Future#get( ) 方法可以获得任务的返回值,该方法会一直阻塞知道任务执行完毕。还可以使用 Future#get(long timeout, TimeUnit unit) 方法,该方法会阻塞一段时间后立即返回,而这时任务可能没有执行完毕。

4.拒绝策略实现详解

线程池中,有三个重要的参数,决定影响了拒绝策略:

  • corePoolSize - 核心线程数,也即最小的线程数。
  • workQueue - 阻塞队列
  • maximumPoolSize - 最大线程数
    当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。

总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

拒绝策略定义

拒绝策略提供顶级接口 RejectedExecutionHandler ,其中方法 rejectedExecution 即定制具体的拒绝策略的执行逻辑。

jdk默认提供了四种拒绝策略:

  • CallerRunsPolicy -当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。
  • AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
  • DiscardPolicy - 直接丢弃,其他啥都没有
  • DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

四种拒绝策略是相互独立无关的,选择何种策略去执行,还得结合具体的业务场景。实际工作中,一般直接使用 ExecutorService 的时候,都是使用的默认的 defaultHandler ,也即 AbortPolicy 策略。

5、关闭线程池

ThreadPoolExecutor 提供了 shutdown( ) 和 shutdownNow( ) 两个方法关闭线程池。原理是首先遍历线程池的工作线程,依次调用 interrupt( ) 方法中断线程,这样看来如果无法响应中断的任务就不能终止。

两者区别是:
如果调用了其中一种方法,isShutdown 方法就会返回 true。当所有的任务都已关闭后, 才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 true。实际应用中可以根据任务是否 一定要执行完毕 的特性,决定使用哪种方法关闭线程池。

6、合理的配置线程池

通常我们可以 根据 CPU 核心数量来设计线程池数量 。

可以通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的物理核心数量。值得注意的是,如果应用运行在一些 docker 或虚拟机容器上时,该方法取得的是当前物理机的 CPU 核心数。

  • IO 密集型 2nCPU
  • 计算密集型 nCPU+1

其中 n 为 CPU 核心数量。

为什么加 1:即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。

三、线程池的运行过程

在这里插入图片描述

当提交一个新任务时,线程池的处理步骤:

1.判断当前线程池内的线程数量是否小于核心线程数,如果小于则新建线程执行任务。否则,进入下个阶段。
2.判断队列是否已满,如果没满,则将任务加入等待队列。否则,进入下个阶段。
3.在上面基础上判断是否大于最大线程数,如果是根据响应的策略处理。否则,新建线程执行当前任务。

线程池的源码比较简单易懂,感兴趣的小伙伴可以自行查看
java.util.concurrent.ThreadPoolExecutor ,在线程池中每个任务都被包装为一个一个的 Worker ,下面简单看下 Worker 的 run( ) 方法:

try {
      while (task != null || (task = getTask()) != null) {
        w.lock();        // If pool is stopping, ensure thread is interrupted;
        // if not, ensure thread is not interrupted. This
        // requires a recheck in second case to deal with
        // shutdownNow race while clearing interrupt
        if ((runStateAtLeast(ctl.get(), STOP) ||
           (Thread.interrupted() &&           runStateAtLeast(ctl.get(), STOP))) &&          !wt.isInterrupted())          wt.interrupt();        try {
          beforeExecute(wt, task);          Throwable thrown = null;
          try {
            task.run();          } catch (RuntimeException x) {
            thrown = x; throw x;
          } catch (Error x) {
            thrown = x; throw x;
          } catch (Throwable x) {
            thrown = x; throw new Error(x);
          } finally {
            afterExecute(task, thrown);          }        } finally {
          task = null;
          w.completedTasks++;          w.unlock();        }      }      completedAbruptly = false;
    } finally {
      processWorkerExit(w, completedAbruptly);    }

可以看到不断的循环取出 Task 并执行,而在任务的执行前后,有 beforeExecute 和 afterExecute 方法,我们可以实现两个方法实现一些监控逻辑。除此之外还可以集合线程池的一些属性或者重写 terminated() 方法在线程池关闭时进行监控。

四、结语

线程池在开发中还是比较常见的,结合不同的业务场景,结合最佳实践配置正确的参数,可以帮助我们的应用性能得到提升。

另外本人整理收藏了20年多家公司面试知识点整理 以及各种知识点整理 下面有部分截图 想要资料的话点击进入领取 暗号CSDN自行领取,希望能对大家有所帮助。
在这里插入图片描述

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/banzhuanhu/article/details/108298516