Java并发编程(线程池篇)

前言

Hello,大家好!我是Leo!

你是否曾经遇到过这样的情况:当你在开发一个多线程应用程序时,你需要同时创建许多线程来完成不同的任务,但是你很快就发现,线程数量太多会导致程序的性能下降,甚至会导致程序崩溃。这时,线程池就像一个救世主一样出现了!它可以帮助我们更好地管理和控制线程的数量,从而提高程序的性能和稳定性。如果你想要了解如何使用线程池来解决这个问题,那么你来对地方了!好了废话不多说,咱们开始吧!!!


关于线程池

什么是池?关于池化思想?

线程池是一种多线程的一种处理形式,处理过程中可以将任务添加到队列中,然后创建线程后自动启动这些任务。

池化思想: 比如:线程池、字符串常量池、数据库连接池

线程池的优势

  • 降低资源消耗:通过重复利用已创建的线程,降低创建线程和销毁的开销。
  • 提高响应速度:当有任务时,不需要创建线程,直接拿池中的线程对象执行。
  • 提高线程的可管理性: 在系统中,线程是稀缺的资源,如果一味创建和销毁线程,不仅会消耗系统资源,而且还降低了系统的稳定性;使用线程的话,可以将线程对象同意分配、调优和监控。

如何构造一个线程池

关于线程池ThreadPoolExecutor查看源码发现它继承了AbstractExecutorService抽象类,而AbstractExecutorService抽象类实现了ExecutorService接口ExecutorService接口又继承了 Executor接口。所以ThreadPoolExecutor间接实现了ExecutorService接口和Executor接口。关系图如下:

线程池的使用

线程池的真正的实现类(也是线程池的标准类)是:ThreadPoolExecutor 类,下边是所有的构造方法:

java 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
 
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;
}

线程池的参数:

  • corePoolSize(必要):代表核心线程数,默认的情况下核心线程数一直存活,当allowCoreThreadTimeout设置为 true,核心线程池也会被超时回收。
  • maximumPoolSize(必要):代表最大线程数,是核心线程数和非核心线程数之和。
  • keepAliveTime(必要):代表线程闲置超时时长,如果超过该时长,非核心线程会被回收。当allowCoreThreadTimeout设置为 true,核心线程池也会被超时回收。
  • unit(必要):指定 keepAliveTime 参数的时间单位。
  • workQueue(必要):任务队列,通过submit方法将任务注册到该队列中。
  • threadFactory(选择):线程工厂,用于指定为线程池创建新线程的方式。
  • handler(选择):拒绝策略,当前线程池达到线程饱和(最大线程数 + 任务队列数)的拒绝策略。

线程池的工作原理

下图是描述线程池工作的原理,同时对上面的参数有一个更深的了解。

线程池把任务的提交和任务的执分开了,当一个任务被提交到线程池之后:

  • 如果此时线程数小于核心线程数,那么就会新起一个线程来执行当前的任务。
  • 如果此时线程数大于核心线程数,那么就会将任务塞入阻塞队列中,等待被执行。
  • 如果阻塞队列满了,并且此时线程数小于最大线程数,那么会创建新线程来执行当前任务。
  • 如果阻塞队列满了,并且此时线程数大于最大线程数,那么会采取拒绝策略。

--

以上就是任务提交给线程池后各种状况汇总,一个很容易出现理解错误的地方就是当线程数达到核心数的时候,任务是先入队,而不是先创建最大线程数。从上述可知,线程池里的线程不是一开始就直接拉满的,是根据任务量开始慢慢增多的,这就算一种懒加载,到用的时候再创建线程,节省资源。


问答时间

当前线程数小于核心线程数,并且线程都处于空闲状态,现在提交一个任务,是新启一个线程还是使用之前创建的线程?

此时线程池会新启一个线程来执行这个新任务,不管之前的线程是否空闲。

如何理解核心线程数?

虽然说线程池是延迟创建线程,但实际是想要快速获得核心线程数的线程。核心线程本质上是线程池需要这些数量的线程来处理任务的,而最大线程数是为了应付突发情况。

举个栗子: 正常情况下施工队只要 5 个人去干活,这 5 人其实就是核心线程,但是由于工头接的活太多了,导致 5 个人在约定工期内干不完,所以工头又去找了 2 个人来一起干,所以 5 是核心线程数,7 是最大线程数。平时就是 5 个人干活,特别忙的时候就找 7 个,等闲下来就会把多余的 2 个辞了。 不论是核心还是非核心线程,在线程池里面都是一视同仁,当淘汰的时候不会管是哪些线程,反正留下核心线程数个线程即可。

线程池有几种状态?

  • RUNNING:能接受新任务,并处理阻塞队列中的任务
  • SHUTDOWN:不接受新任务,但是可以处理阻塞队列中的任务
  • STOP:不接受新任务,并且不处理阻塞队列中的任务,并且还打断正在运行任务的线程,就是直接撂担子不干了!
  • TIDYING:所有任务都终止,并且工作线程也为0,处于关闭之前的状态
  • TERMINATED:已关闭。

线程池的生命周期是怎样的?

线程池有哪些工作队列?

ThreadPoolExecutor的工作队列可以分为两类:无界队列和有界队列

  • ArrayBlockingQueue

是基于一个数组结构的有界阻塞队列,该队列按照先进先出(FIFO)原则对元素进行排序。

  • LinkedBlockingQueue

是基于链表结构的阻塞队列,队列按照先进先出(FIFO)排序元素,吞吐量要高于ArrayBlockingQueue。

  • SynchronousQueue

不存储元素的阻塞队列。每个插⼊操作必须等到另⼀个线程调⽤移除操作,否则插⼊操作⼀直处于阻塞状态。

  • PriorityBlockingQueue

具有优先级的⽆限阻塞队列。

  • DelayQueue

是任务定时周期的延迟执⾏的队列。根据指定的执⾏时间从⼩到⼤排序,否则根据插⼊到队列的先后排序。

几种常见的线程池使用场景

  • newSingleThreadExecutor

创建⼀个单线程化的线程池,它只会⽤唯⼀的⼯作线程来执⾏任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执⾏。

  • newFixedThreadPool

创建⼀个定长线程池,可控制线程最⼤并发数,超出的线程会在队列中等待。

  • newCachedThreadPool

创建⼀个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若⽆可回收,则新建线程。

  • newScheduledThreadPool

创建⼀个定长线程池,⽀持定时及周期性任务执⾏。

  • newWorkStealingPool

⼀个拥有多个任务队列的线程池,可以减少连接数,创建当前可⽤ cpu 数量的线程来并⾏执⾏。

shutdown 和 shutdownNow 区别

shutdown 和 shutdownNow 均可⽤于关闭线程池.

  • shutdown

当我们调⽤ shutdown ⽅法后,线程池将不再接受新的任务,但也不会去强制终⽌已经提交或者正在执⾏中的任务。

  • shutdownNow

当调⽤ shutdownNow ⽅法后,会向正在执⾏的全部任务发出 interrupt() 停⽌执⾏信号,对还未开始执⾏的任务全部取消,并且返回还没开始的任务列表。

相关推荐
LayZhangStrive6 分钟前
融360 一面
java·面试·后端开发
城管不管31 分钟前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源
Java小白笔记35 分钟前
Windows系统免软件命令激活
java·网络·人工智能·windows·ai·ai编程
爱敲键盘的猴子43 分钟前
深入理解 Java HashMap
java·hashmap
霸道流氓气质44 分钟前
Java开发者AI编程常用MCP、Skills、Rules全景汇总
java·开发语言·ai编程
天涯醉青楼1 小时前
Spring Boot 接口返回 Bean 时字段名“变形”的深度剖析与解决方案
java
步行cgn1 小时前
MyBatis 高级映射与延迟加载完全指南
java·tomcat·mybatis
thefool1122661 小时前
从中序与后序遍历序列构造二叉树
java
吴声子夜歌1 小时前
Java面试题——JVM(二)
java·开发语言·jvm
CRMEB1 小时前
商城大促活动策划全流程:从定目标到复盘四阶段
java·开发语言·人工智能·ai·开源·php