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

相关推荐
跳动的梦想家h几秒前
黑马点评 秒杀下单出现的问题:服务器异常---java.lang.NullPointerException: null(已解决)
java·开发语言·redis
苹果醋33 分钟前
前端面试之九阴真经
java·运维·spring boot·mysql·nginx
哎呦没26 分钟前
Spring Boot OA:企业办公自动化的高效路径
java·spring boot·后端
前端fighter27 分钟前
js基本数据新增的Symbol到底是啥呢?
前端·javascript·面试
真心喜欢你吖27 分钟前
Spring Boot与MyBatis-Plus的高效集成
java·spring boot·后端·spring·mybatis
2402_8575893631 分钟前
企业办公自动化:Spring Boot OA管理系统开发与实践
java·spring boot·后端
G丶AEOM1 小时前
JVM逃逸分析机制
java·jvm
无聊写博客1 小时前
JDK、JRE、JVM的区别
java·开发语言·jvm
message丶小和尚1 小时前
SpringBoot升级全纪录之项目启动
java·spring boot·mybatis
豆子熊.1 小时前
外包干了3年,技术退步明显...
软件测试·selenium·测试工具·面试·职场和发展