线程池的介绍
常量池:
在讨论线程池之前,我们先说一下常量池:
Java常量池是Java虚拟机(JVM)中的一块内存区域,用于存储字符串常量和基本类型常量。字符串常量池是常量池中的一个重要部分,它用于存储所有的字符串常量。当程序中有一个字符串常量时,JVM会首先检查常量池中是否已经存在该字符串,如果存在,则直接返回引用,如果不存在,则将该字符串添加到常量池中,并返回引用。
基本类型常量包括整数、浮点数、布尔值等。基本类型常量池用于存储这些基本类型的常量。当程序中使用了一个基本类型常量时,JVM会将它存储到常量池中,并返回引用。
Java常量池的存在主要是为了减少内存的使用,提高程序的执行效率。通过共享常量的方式,可以避免重复创建相同的常量,节约内存空间。在Java中,可以使用关键字final来定义常量,保证常量的唯一性和不可修改性。
线程池:
线程池顾名思义,就是用来储存线程的。
Java提供了线程池来管理和执行多线程任务。线程池是一种有限容量的线程集合,它可以重复使用线程,从而减少线程创建和销毁的开销,提高效率(因为频繁的创建和销毁线程,也会有不必要的开销)。
把线程创建好后,放到一个地方(类似于数组),需要用的时候,随时去取,用完后放回“池子”。
为什么我们认为直接创建线程的开销比从线程池中取线程开销更大呢?
-
创建线程的开销:创建一个线程需要操作系统进行资源分配,包括为线程分配内存空间以及设置线程的上下文等。相比之下,线程池中已经预先创建好了一定数量的线程,因此不需要再花费时间和资源去创建线程。
-
线程启动和销毁的开销:每次创建一个新线程,需要进行线程的启动和销毁操作,包括设置线程的起始点和清理线程资源等。而线程池中的线程已经启动,可以复用,不需要再进行这些操作。
-
线程调度的开销:线程池中的线程已经在运行队列中进行管理和调度,可以通过调度算法更高效地进行线程的切换和调度,避免了频繁地创建和销毁线程带来的开销。
-
资源利用率:直接创建线程的方式往往会导致线程的数量过多,而线程在执行过程中很可能会出现等待资源的情况,造成资源的浪费。而线程池可以根据实际情况动态调整线程的数量,提高资源的利用率。
-
线程的创建和消费都需要操作系统的内核参与(不可控),但是从线程池里面取出现成的线程只需要通过代码实现的(可控)
常见的线程池实现类是ThreadPoolExecutor
,它提供了很多配置选项,可以根据需求来创建不同类型的线程池。
1.使⽤ Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
2.返回值类型为 ExecutorService(可以强转成ThreadPoolExecutor)
3.通过 ExecutorService.submit 可以注册⼀个任务到线程池中.
Executors 创建线程池的几种⽅式:
以下是一些常用的线程池参数:
总共七个参数:
经典面试题(参数表示什么意思):
1.int corePoolSize 【核心线程数】,最少有多少线程(线程池一创建,这些线程也要随之创建,直到整个线程池销毁,这些线程才会销毁)
2.int maximumPoolzSize【最大线程数】,最大线程数=核心线程数+非核心线程数(非核心线程数是自适应的【繁忙时就创建,不繁忙就销毁】)
3.long keepAliveTime 【非核心数允许最大的空闲时间】,超出了时间的非核心线程会被收回
4.TimeUnit unit 【枚举】,unit是 keepAliveTime 的时间单位。它用于指定 keepAliveTime 参数的时间单位,即指示线程的存活时间。unit 参数可以是 TimeUnit 中的枚举值之一,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。通过 unit 参数,我们也可以灵活地指定线程的存活时间单位,例如指定线程存活时间为 5 秒、10 毫秒等。
5.BlockingQueue<Runnable> workQueue【阻塞队列】,线程池本质也是生产者消费者模型,调用submit方法,就是在生产任务,线程池的线程就是在消费任务。当线程池中的任务满了之后,多余的任务就会被放到此队列中进行等待。
6.ThreadFactory threadFactory【工厂模式】,工厂模式也是一种设计模式,其核心是通过静态方法,把构造对象new以及各种属性初始化的过程封装起来了(通过一个工厂类来封装对象的创建过程,客户端只需要与工厂类交互,而不需要直接实例化具体的类)
简单的工模式:
简单工厂模式通过一个工厂类来创建不同类型的对象,客户端只需要传递一个参数给工厂类,工厂类根据参数决定创建哪种对象。
// 产品接口
interface Product {
void use();
}
// 具体产品A
class ProductA implements Product {
@Override
public void use() {
System.out.println("Using Product A");
}
}
// 具体产品B
class ProductB implements Product {
@Override
public void use() {
System.out.println("Using Product B");
}
}
// 简单工厂类
class SimpleFactory {
public static Product createProduct(String type) {
if (type.equals("A")) {
return new ProductA();
} else if (type.equals("B")) {
return new ProductB();
}
return null;
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Product productA = SimpleFactory.createProduct("A");
productA.use();
Product productB = SimpleFactory.createProduct("B");
productB.use();
}
}
工厂模式可以隐藏对象的创建逻辑,使代码更加灵活、可维护和可扩展。
7.RejectExecutionHandle handler(最重要,最复杂的)【拒绝策略】
当submit方法把任务添加到任务队列中(阻塞队列),队列满了,再入队列,就会阻塞,一般不允许阻塞太多。对于线程池来说,发现队列满了,继续入队列的话,不会真的触发“入队列”操作,不会阻塞,而是执行拒绝策略的相关代码。
拒绝策略分为:
ThreadPoolExecutor.AbortPolicy(默认):当线程池的工作队列已满且线程数量已达到最大线程数时,新提交的任务将会被丢弃,并抛出RejectedExecutionException异常。
ThreadPoolExecutor.CallerRunsPolicy:当线程池的工作队列已满且线程数量已达到最大线程数时,新提交的任务将会使用提交任务的线程来执行。
ThreadPoolExecutor.DiscardOldestPolicy:当线程池的工作队列已满且线程数量已达到最大线程数时,会将最早的任务从队列中移除,然后把新的任务加入队列。
ThreadPoolExecutor.DiscardPolicy::当线程池的工作队列已满且线程数量已达到最大线程数时,新提交的任务将会被丢弃,但不会抛出异常。
实现一个线程池:
import java.util.concurrent.*;
class MyThreadPool{
private BlockingDeque<Runnable> queue=new LinkedBlockingDeque<>();//阻塞队列
//通过这个方法将任务添加到队列中
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
//n表示线程数,创建一个固定数量的线程池
public MyThreadPool(int n){
for(int i=0;i<n;i++){
Thread t=new Thread(()->{
while(true) {
try {
Runnable r= queue.take();
r.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThreadPool mythreadpool=new MyThreadPool(4);//线程池中4个线程
for(int i=0;i<=1000;i++){
mythreadpool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
}
但是有个问题,就是任务执行完了,但是我们发现进程没有关闭,是因为线程池里面的线程都是前台线程,前台线程都结束了,线程才会结束,那为什么这些前台线程吗明明没有任务了,却还没有结束,因为WorkerThread内部的循环会尝试从任务队列中取出任务来执行。如果此时任务队列是空的,调用take()方法会导致线程阻塞,直到队列中有新的任务或者线程被中断。
结束线程池的方法:
1.shutdownNow()方法后,会立即使每个WorkerThread线程结束,从而打破了阻塞状态,使得线程池结束运行
2.调用shutdown()方法,这个方法不会接受新的任务,但会等已经提交的任务执行完,然后结束线程池。