使用多进程进行并发编程,会频繁的创建销毁进程,效率比较慢,所以引入了线程,线程使用复用资源的方式提高了创建销毁的效率,但是随着创建线程的频率进一步提高,开销仍然无法忽略不计了。

要想办法优化此处线程的创建销毁效率,方案有两种:

  1. 引入轻量级线程——纤程/协程。即Java 21里引入的”虚拟线程“。协程的本质是程序员在用户态代码中进行调度,不是靠内核的调度器调度的,节省了很多调度上的开销。
  2. 线程池。把要使用的线程提前创建好,用完了不销毁等待下次使用。每次创建一个新线程需要为该线程分配堆栈内存、初始化线程管理数据结构等等,这些操作都需要消耗一定的系统资源。使用线程池可以重复利用已经创建的线程,避免了这种开销。

1. Java标准库中的线程池

1.1 ThreadPoolExecutor类

 ThreadPoolExecutor 类提供了如下四个构造方法:

java多线程线程池异步请求 返回结果_c语言

我们重点理解最后一个:

  1. corePoolSize(核心线程数):这是线程池中始终保持活动状态的线程数量。即使没有任务需要执行,这些线程也会保持活跃,可以理解为最小线程数。
  2. maximumPoolSize(最大线程数):最大线程池大小。当提交的任务数大于核心线程池大小并且工作队列已满时,线程池会创建新线程来处理任务,但新线程数量不会超过最大线程池大小。
  3. keepAliveTime(非核心线程空闲时间):非核心线程空闲时间。当线程池中的线程数量大于核心线程池大小时,如果线程空闲时间超过了该参数所指定的时间,那么这个线程就会被销毁,直到线程数量等于核心线程池大小。
  4. unit(时间单位):keepAliveTime参数的时间单位,可以是秒、毫秒等。
  5. workQueue(工作队列):任务队列。用于存储尚未执行的任务。线程池会从任务队列中取出任务并进行处理。
  6. threadFactory(线程工厂):用于创建新线程的工厂。可以通过自定义线程工厂来设置线程的名称、优先级等属性。
  7. handler(拒绝策略):当线程池无法处理新提交的任务时,将使用此策略来处理。常见的拒绝策略有:
    AbortPolicy:直接抛出异常,不处理任务。
    CallerRunsPolicy:只用调用者所在的线程来运行任务。
    DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试重新提交当前任务。
    DiscardPolicy:直接丢弃任务,不处理。

需要注意的是,corePoolSize和maximumPoolSize参数决定了线程池的容量大小,而workQueue则决定了能够存储多少个等待执行的任务。如果任务量过大,超出了workQueue的容量,再加上全部的线程都在执行任务的情况下,那么就会触发线程池的拒绝策略来处理这些任务,从而保证线程池不会因为资源被耗尽而崩溃。 

解释:

工厂模式:工厂模式是一种常见的设计模式,通过专门的  工厂类 / 工厂对象 来创建指定的对象,例如这里的 ThreadFactory。

工厂模式本质上是为了给Java语法填坑的,举个例子:

我要表示平面上的一个点,可以用笛卡尔坐标系,也可以用极坐标系:

 

java多线程线程池异步请求 返回结果_java_02

 很明显,这样的代码无法通过编译,因为,这两个构造方法无法构成重载。

 为了解决上述问题,就引入了工厂模式,使用普通的方法来创建对象,就是把构造方法封装了一层:

java多线程线程池异步请求 返回结果_线程池_03

此时这两个方法就叫工厂方法,如果把工厂方法放到其他的类里,这个类就叫工厂类,总的来说,通过静态方法封装new 操作,在方法内部设定不同的属性完成对象初始化,构造对象的过程就是工厂模式。

1.2 Executors 工厂类

ThreadPoolExecutor 类本身用起来比较复杂,所以标志库中还提供了另一个版本,把ThreadPoolExecutor 给封装了一下。即 Executors 工厂类,通过这个类来创建出不同的线程池对象(在内部把ThreadPoolExecutor 创建好了,并设置了不同的参数)。

 

java多线程线程池异步请求 返回结果_任务队列_04

SingleThreadExecutor:只包含单个线程的线程池
ScheduledThreadPool:定时器类似物,能延时执行任务
CachedThreadPool:线程数目能动态扩容
FixedThreadPool:线程数目固定

使用示例:

public class ThreadDemo28 {
    public static void main(String[] args) {
        //创建一个四个线程的线程池
        ExecutorService service = Executors.newFixedThreadPool(4);
        //通过submit方法添加任务
        service.submit(() -> {
            System.out.println("Ting");
        });
    }
}

ThreaPoolExecutor  也是通过submit 添加任务,只是构造方法不同。

希望高度定制化时使用 ThreadPoolExecutor 

 创建线程池的时候,怎么设置线程池的线程数量比较合适?

这个情况需要具体问题具体分析:

一个线程是CPU密集型任务,还是 IO 密集型任务

CPU密集型任务:这个线程大部分都在CPU上执行,如果所有线程都是CPU密集型的,这个时候建议线程数量不要大于设备的逻辑核心数量。

IO 密集型任务:这个线程大部分时间都在等待 IO ,如果所有线程都是 IO 密集型的,这个时候线程数量可以很多

上述两种情况是极端情况,大部分情况都是,有一部分线程是 CPU 密集型,一部分是 IO 密集型,所以,更适合的做法是通过测试的方式找到合适的线程数目。 

即尝试给线程池设定不同的线程数目分别进行性能测试,对比每种线程数目下,总的时间开销,和系统资源占用的 开销,找到一个最合适的值。

1.3 线程池的执行流程

  1. 当新加入一个任务时,先判断当前线程数是否大于核心线程数,如果结果为 false,则新建线程并执行任务;
  2. 如果结果为 true,则判断任务队列是否已满,如果结果为 false,则把任务添加到任务队列中等待线程执行
  3. 如果结果为 true,则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务
  4. 如果结果为 true,执行拒绝策略。

2. 简单实现一个线程池

 我们先来整理一下,实现一个线程池需要哪些内容:

  • 一个任务队列:记录要执行的任务
  • submit方法:添加任务
  • 构造方法:指定线程的数量以及创建,运行线程
class MyTreadPoolExecutor {
    //任务队列,这里使用一个阻塞队列
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);

    //构造方法
    public MyTreadPoolExecutor(int num) {
        for(int i = 0; i < num; i++) {
            //创建线程
            Thread t = new Thread(() -> {
                //循环取出任务并执行
                while(true) {
                    try {
                        queue.take().run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }

    //submit
    public void submit(Runnable runnable) {
        //添加任务
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class TreadDemo29 {
    public static void main(String[] args) throws InterruptedException {
        MyTreadPoolExecutor pool = new MyTreadPoolExecutor(4);

        for(int i = 0; i < 1000; i++) {
            int n = i;
            pool.submit(() -> {
                System.out.println("线程:" + Thread.currentThread().getName() + "执行了任务:" + n);
            });
        }
    }
}

运行效果:

java多线程线程池异步请求 返回结果_线程池_05

注意这里的任务执行无序的原因是,多个线程并发执行。

例如:任务 0 刚被某个线程拿到,改线程就被调度出了cpu 此次任务 2 就可能被拿到并执行了