线程池
谈起线程池之前,我们可以联想到常量池,那什么是常量池呢?
常量池:字符串常量,在 Java 程序最初构建的时候,就已经准备好了。等程序运行的时候,这样的常量也就加载到内存中了。因此剩下了构造/销毁的开销。
其实,在计算机中,池 这个词,就只有这一个意思,表示的含义都是一样的 。
线程池,就是为了让我们高效的创建销毁线程的。我们不妨想想最初引入线程的原因:频繁创建销毁进程,太慢了。
随着互联网的发展,随着我们对性能的要求更进一步,咱们现在觉得,频繁创建销毁线程,开销有些不能接受了。解决方案有两个:1.线程池 2. 协程(纤程,轻量级线程)。
所以,线程池其实就是:把线程提前创建好,放到一个地方(放到类似于数组),需要用的时候,随时去取,用完了还回到池子中。
但是,其实还有小问题~~
为啥我们认为,直接创建线程开销比从池子里取线程更大呢???
这里我们就不得不说一下操作系统的 用户态 和 内核态 了。
一个操作系统 = 内核 + 配套的应用程序
-
内核是包含操作系统的各种核心功能
1)管理硬件设备。
2)给软件提供稳定的运行环境
一个操作系统,内核就是一份,一份内核,要给所有的应用程序提供服务支持的。
如果有一段代码是应用程序中自行完成的,则整个执行过程是可控的。
如果有一段代码,需要进入到内核中,由内核负责完成一系列工作,这个过程是不可控的,咱们程序员写的代码干预不了。
因此,通常认为,可控的过程要比不可控的过程更高效。
从线程池取现成的线程,纯应用程序代码就可以完成。【可控】
从操作系统创建新的线程,就需要操作系统内核配合完成。【不可控】
所以使用线程池,就可以省下应用程序切换到内核中运行这样的开销。
Java标准库也提供了直接使用的线程池(ThreadPoolExecutor -> 线程池里准备好一些线程,让这些线程执行一些任务)
核心方法,submit(Runnable),通过Runnable描述一段要执行的任务。
通过submit任务放到线程池中,此时线程池里的线程会执行这样的任务。
但有一个比较重要的是,构造这个类的时候,构造方法,比较麻烦。(参数有点多)
而且,Java的线程池,里面包含几个线程,是可以动态调度的。任务多的时候,自动扩容成更多的线程,任务少的时候,把额外的线程干掉,节省资源。
此处,这里讲一下最后一个,因为他的参数最多。都含上面的参数。
-
int corePoolSize 核心线程数 -> 至少有多少个线程,线程池一创建,这些线程也要随之创建。直到整个线程池销毁,这些线程才会销毁。
-
int maximumPoolSize 最大线程数 -> 最大线程 + 非核心线程(自适应) 就是不繁忙就销毁,繁忙就在创建。(当然,线程也不是越多越好)
举个例子:我们可以简单的理解成 线程池 => 公司, 核心线程 => 正式员工 , 非核心线程 => 实习生。需要的时候就招聘实习生,不需要的时候就裁掉。但是呢,正式员工不敢乱裁,如果赔偿不到位,劳动仲裁就够喝一壶。
-
long keepAliveTime 非核心线程允许空闲的最大时间 也就是允许实习生摸鱼的时间,如果实习生连续一个月都没有啥活了,就可以考虑优化掉。
-
TimeUnit unit 枚举。
-
BlockingQueue workQueue 工作队列 (选择使用数组/链表,指定capacity,指定是否要带有优先级/比较规则)线程池,本质上也是 生产者消费者模型调用submit就是在生产任务,线程池里的线程就是在消费任务。
-
ThreadFactory threadFactory 给线程提供的工厂类(统一的构造并初始化线程),工厂模式 (也是一种设计模式,和单例模式是并列的关系)-> 用来弥补构造方法的缺陷的。 线程中有一些属性可以设置,线程池是一组线程。
-
※RejectedExecutionHandler handler 拒绝策略(整个线程池七个参数中,最重要的,最复杂的)。submit把任务添加到任务队列中(任务队列就是阻塞队列),队列满了,再添加,就阻塞。(一般不希望程序阻塞太多)。对于线程池来说,发现入队列操作时,队列满了,不会真的触发“入队列操作”,不会真阻塞,而是执行拒绝策略相关代码。 如果调用submit就阻塞(业务逻辑中的线程调用submit)就会使这个线程就没办法干别的事情了,不是一个好的选择。
(这个线程要响应用户的请求,阻塞了,用户迟迟拿不到请求的响应,用户等很久,直观上看到的现象 是“卡了”,与其是“卡了”不如告诉直接我“失败”)
工厂模式
工厂模式是Java中常用的设计模式之一,它主要用于创建对象,同时将对象的创建和使用过程分离,提高了代码的可维护性和可拓展性。工厂模式主要有三种类型:简单工厂模式,工厂方法模型和抽象工厂模式。这里就拿简单工厂模式举例。
简单工厂模式,又称为静态工厂模式,通过一个工厂类根据传入的参数决定创建哪种类型的对象。这种模式的优点是易于理解和实现,但缺点是随着产品类的增加,工厂类的代码也需要修改,这违反了开闭原则。(对扩展开放,对修改关闭)。
就比如,我想在一个平面上创建一个点,我们可以用极坐标表示这个点,也可以用平面坐标系来表示。
但是由于这两个构造方法的个数,参数类型相同,无法构成重载。所以,对于这种情况,我们就需要用工厂模式来解决这种情况。(C++和Java共有的一个问题:构造方法的名字是固定的,想要提供不同的版本,就需要通过重载,有时候不一定能构成重载),我们创建一个工厂类,来对 目标对象 进行实例化,工厂类来对 目标对象 进行初始化。
其中,
称为工厂方法。
提供工厂方法的类称为工厂类。
工厂方法的核心,通过静态方法,把构造对象 new 的过程,各种属性初始化的过程,封装起来了。提供多组静态方法,实现不同情况的构造。
Executors
在Java标准库,也提供了另一组类,针对 ThreadPoolExecutor 进行了进一步封装,简化线程池的使用,也是基于工厂设计模式。
- 使⽤ Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池。
- 返回值类型为 ExecutorService。
- 通过 ExecutorService.submit 可以注册⼀个任务到线程池中。
Executors 创建线程池的⼏种⽅式:
newFixedThreadPool: 创建固定线程数的线程池(核心线程数和最大线程数一样)
newCachedThreadPool: 创建线程数⽬动态增⻓的线程池.(最大线程数是一个很大的数字(线程可以无限增加))
newSingleThreadExecutor: 创建只包含单个线程的线程池.
newScheduledThreadPool: 设定 延迟时间后执⾏命令,或者定期执⾏命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装
实现固定线程个数的线程池
- 核⼼操作为 submit, 将任务加⼊线程池中
- 使⽤ Worker 类描述⼀个⼯作线程. 使⽤ Runnable 描述⼀个任务.
- 使⽤⼀个 BlockingQueue 组织所有的任务
- 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执⾏
(线程池,最核心的就是submit 这样的操作,往线程池中,添加任务(任务就是Runnable)。得有线程来执行队列中的任务,所以我们在构造方法中,把线程创建处来,随时有新的任务要被添加进去,咱们的线程就需要持续不断的尝试读取任务,然后取到了就执行,没取到就阻塞等待)
在执行的时候我们会发现虽然100 个任务完成了,但进程并没有结束
这是因为线程池里的这些线程,还在take阻塞的(等待),线程池中的线程,是前台线程,阻止进程结束。
shutdown 能够把线程池里的线程全部关闭,但是不能保证线程池内的任务一定能全部执行完毕。
所以,如果需要等待线程池内的任务全部执行完毕,需要调用 awaitTermination 方法。
(线程池最主要的还是解决服务器这边的开发的问题,上古时期,服务器如何处理多个客户端的请求?基于多进程的模型。每次有个客户端请求过来了,服务器这边都创建一个 进程 给这个客户端提供服务(读取请求,解析请求,返回响应…)后来,频繁创建销毁进程,效率比较低,引入了线程,模型就成了,每个客户端都分配一个线程,提供服务。但随着客户端越来越多,发现频繁创建销毁线程,这个事情也变得比较低效了,又引入了线程池。线程池里,提前准备好了 10 个线程,有100个客户端把请求发过来,把这100个客户端的请求,封装成 任务(Runnable)添加到线程池里,线程池中有着 10 个线程负责处理这 100 个任务,这个过程就不涉及线程的创建销毁了)。