并发编程——4.线程池

这篇文章我们来讲一下线程池的相关内容

目录

1.什么是线程池

1.1为什么要用线程池

1.2线程池的优势

2.线程池的使用

3.线程池的关闭

4.线程池中的execute和submit方法的一些区别

5.线程池的参数和原理

6.自定义线程池

7.总结


1.什么是线程池

1.1为什么要用线程池

首先,我们要清楚这样的一个问题,我们为什么要用线程池?

思考下面的这样的一个场景:某线上商城做秒杀活动,1秒内有10万个请求打来,我们的服务器能在1秒内创建10万个线程吗?显然是不合理的。其次,我们使用线程时会伴随着线程的创建和消耗,当线程数量很多时,这会占用机器的资源,所以这也不合理。为了解决上面提到的问题,所以我们使用了线程池。

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

1.2线程池的优势

线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行

它的主要特点为:线程复用;控制最大并发数;管理线程。

线程池的优势:

  • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

2.线程池的使用

对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent . Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用 Executors工程类来创建线程池对象。Java类库提供了许多静态方法来创建一个线程池:

Executors类中创建线程池的方法如下:

  • a、newFixedThreadPool创建一个固定长度的线程池,当到达线程最大数量时,线程池的规模将不再变化。
  • b、newCachedThreadPool创建一个可缓存的线程池,如果当前线程池的规模超出了处理需求,将回收空的线程;当需求增加时,会增加线程数量;线程池规模无限制。
  • c、newSingleThreadPoolExecutor创建一个单线程的Executor,确保任务对了,串行执行
  • d、newScheduledThreadPool创建一个固定长度的线程池,但是是可以定时的进行调度

下面我们就通过代码来看一下各种方法的使用

这个其实没啥好说的,因为这个用的是Java中提供的线程池的工具类,是很简单的,直接用就行。

3.线程池的关闭

下面看一下线程池的关闭

我们创建一个线程池,在使用完毕后,一定要关闭线程池,关闭线程池的代码要放在finally里面,目的是为了避免程序出错,导致线程池没有关闭。我们关闭线程池后,线程池中正在执行的线程不会立刻被停止,它会等到该线程执行完毕后再关闭,即线程池是会等待线程池中所有的线程都被执行完才关闭

为什么要关闭线程池?为了避免线程池中的线程一直占用系统资源,造成内存泄漏

下面看一下代码:

注意区分shutdown和shutdownNow二者的区别!

4.线程池中的execute和submit方法的一些区别

下面,我们来看一下线程池中的execute方法和submit方法的区别

区别一:

execute方法的入参只能是Runnable,而submit方法的入参可以是Runnable,也可以是Callable

下面看一下代码:

区别二:

execute的返回值为void类型,而submit的返回值为Future类型

下面看一下代码:

区别三:

execute 会在子线程中抛出异常,但是主线程中捕捉不到。而submit是不会立刻抛出异常,它会将此异常暂时存起来,如果想要将该异常抛出,需要用Future对象接收submit的返回值,然后调用Future的get方法。

下面看一下代码:

5.线程池的参数和原理

下面,我们来看一下线程池的参数和原理

首先,我们看一下我们四种创建线程池的方式,然后Ctrl+单击,看一下它的源码:

我们会发现,这四种创建线程池的源码基本相同,都是返回了一个ThreadPoolExecutor对象,区别就在于里面的参数不一样。我们再单击看一下ThreadPoolExecutor的源码:

如上图所示,下面我们来介绍一下ThreadPoolExecutor中参数的意思

  • corePoolSize:核心线程数量(这个不用过多解释)
  • maximumPoolSize:最大线程数量(就是这个线程池最大能开辟的线程数量)
  • KeepAliveTime:非核心线程的空闲状态的存活时间(就是如何该线程不是核心线程,并且它是空闲的,那么它的存活时间是多少,是long类型的数字)
  • unit:非核心线程的空闲状态的存活时间的单位
  • workQueue:工作队列(阻塞队列)
  • threadFactory:线程工程(创建线程用的,就是一个接口,然后平时使用的就是创建默认线程)
  • handler:拒绝策略(就是当我们的核心线程数,阻塞队列,最大线程数都满了的情况的下,还有线程来,那么就按照这个拒绝策略进行拒绝)

下面就整个流程进行一下梳理:

首先,我们进行提交任务,然后程序会判断你提交的任务数量是否大于你创建线程池的核心线程数量,如何不大于,OK,那么程序开始创建线程,执行任务;如果大于了核心线程数量,那么多余的任务进入阻塞队列里面进行等待,如果这时阻塞队列满了,那么程序会判断你创建线程池的最大线程数量是否满了,如果没满,那么程序根据最大线程数量来创建线程,执行任务,如果这时最大线程数量也满了,那么就执行拒绝策略,多余的任务就被拒绝了,不接受了。

这里对最大线程数量和非核心线程空闲状态存活时间进行一下场景解释:
假设现在有一个促销秒杀活动,比如双十一、618等。你的网站日常活跃用户只有5000万,所以你的程序的核心线程数设置为5000万,然后最大线程数设置为7000万,但是在活动促销的那两个小时,你的网站的活跃用户达到了1亿,那么你就不可能再把最大线程数设置为7000万了,而是设置为1亿,但是你只有那两个小时的用户活跃数是1亿,所以你不可能一直让那1亿个线程开着,所以你将非核心线程的空闲存活时间设置为2h,时间一过,如果该非核心线程是空闲状态,那么该线程就被销毁,这样就可以节约资源了。

下面看一下拒绝策略:

这个了解一下就可以了。

6.自定义线程池

上面我们学习了线程池的核心源码和其中的一些参数的含义,下面我们来书写一个自定义的线程池,然后我们再来具体的分析一下

代码如下:

然后,我们来看一下输出结果:

下面再用一张图来解释一下吧:

其实这是很好理解的

7.总结

这篇文章我们主要介绍了一下线程池的相关内容,包括什么是线程池,怎么创建或者说怎么使用线程池,线程池的注意点,线程池中两个方法的区别,以及如何自定义线程池(线程池的源码会在源码篇章中写),这部分是比较重要的内容,要掌握。

相关推荐

  1. 探索并发编程:深入理解线

    2024-03-31 15:58:03       18 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-31 15:58:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-31 15:58:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-31 15:58:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-31 15:58:03       20 阅读

热门阅读

  1. 【shell】shell实现等待用户输入

    2024-03-31 15:58:03       12 阅读
  2. 【华为OD机试C++】进制转换

    2024-03-31 15:58:03       14 阅读
  3. [单调队列] 滑动窗口

    2024-03-31 15:58:03       13 阅读
  4. 一文总结vue和react的区别

    2024-03-31 15:58:03       14 阅读
  5. zookeeper命令详解1

    2024-03-31 15:58:03       19 阅读
  6. Zookeeper中的ACL 权限控制机制

    2024-03-31 15:58:03       13 阅读
  7. 初识 内嵌iframe

    2024-03-31 15:58:03       14 阅读