写点什么

Java 并发编程(线程池篇)

作者:java易二三
  • 2023-08-31
    湖南
  • 本文字数:3586 字

    阅读完需:约 12 分钟

前言

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


关于线程池

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

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

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

线程池的优势

  • 降低资源消耗:通过重复利用已创建的线程,降低创建线程和销毁的开销。

  • 提高响应速度:当有任务时,不需要创建线程,直接拿池中的线程对象执行。

  • 提高线程的可管理性: 在系统中,线程是稀缺的资源,如果一味创建和销毁线程,不仅会消耗系统资源,而且还降低了系统的稳定性;使用线程的话,可以将线程对象同意分配、调优和监控。

如何构造一个线程池

关于线程池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() 停⽌执⾏信号,对还未开始执⾏的任务全部取消,并且返回还没开始的任务列表。

用户头像

java易二三

关注

还未添加个人签名 2021-11-23 加入

还未添加个人简介

评论

发布
暂无评论
Java并发编程(线程池篇)_Java_java易二三_InfoQ写作社区