【多线程初阶】多线程案例之线程池

文章目录

  • 前言
  • 1. 什么是线程池
    • 1.1 线程池的优势
  • 2. 标准库中的线程池
    • 2.1 聊聊工厂模式
    • 2.2 Executors 创建线程池的几种方式
    • 2.3 ThreadPoolExecutor 构造方法中的几个参数
      • 2.3.1 RejectedExecutionHandler handler 的几个拒绝策略
  • 3. 自己实现一个线程池
  • 总结


前言

本文主要给大家讲解多线程的一个重要案例 — 线程池.

关注收藏, 开始学习吧


1. 什么是线程池

在讲解线程池是什么之前, 我们先简单聊一聊 “池” 的概念, 在我们学习中, “池” 是一个非常重要的思想方法, 之前听过有内存池, 进程池, 连接池, 常量池等等, 这里的 “池” 其实本质概念上都是一样的.

那么什么是 “池” 呢, 不站在道德层面上来讲, 其实就是我们常说的鱼塘, 鱼塘里都是鱼, 也就是常听到的 “备胎”, 这样就容易理解了吧? 同时和池子里的多个目标搞暧昧, 也就是扩大备胎池, 是不是在某种意义上就提高了谈恋爱的效率呢.

在这里我们的线程池也是一样的, 如果我们只创建销毁一个线程的话, 成本可能并不高, 当我们需要频繁的创建 / 销毁线程, 此时创建销毁线程的成本就不能被忽视了, 因为数量太多了. 我们就需要线程池了. 我们提前创建好一些线程放在一个池子里, 当我们后续需要使用线程时, 直接从池子里拿即可, 当线程不再使用时, 就放回池子里, 就可以大大减少我们频繁创建 / 销毁线程的成本.

1.1 线程池的优势

  1. 降低资源消耗:减少线程的创建和销毁带来的性能开销。
  2. 提高响应速度:当任务来时可以直接使用,不用等待线程创建
  3. 可管理性: 进行统一的分配,监控,避免大量的线程间因互相抢占系统资源导致的阻塞现象。

那么从线程池里取, 就比从系统这里创建线程更高效吗?

  • 如果是从系统这里创建线程, 需要调用系统API, 进一步的由操作系统内核来完成线程的创建过程, 而我们操作系统中的内核是给所有的进程提供服务的, 在这里是不可控的.
  • 如果是从线程池这里获取线程, 上述的内核中进行的操作, 已经提前做好了, 作为用户, 我们只需要去池子里拿即可, 是纯用户态的, 也是用户自己可控的.

2. 标准库中的线程池

在 Java 标准库中, 也提供了现成的线程池.

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中.
public class ThreadDemo22 {
    // 线程池
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        // 注册 1000 个任务到线程池中
        for (int i = 0; i < 1000; i++) {
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

可以看到, 在创建线程池时, 并不是使用 new 一个对象来进行实例化的. 使用的是一个工厂方法 Executors.newFixedThreadPool(), 而 Executors 则是工厂类.

我们来简单谈谈设计模式中的一个经典模式 ---- 工厂模式.

2.1 聊聊工厂模式

顾名思义, 工厂就是用来生产的, 是用来生产对象的, 一般我们创建对象时, 都是使用 new, 通过构造方法来实例化一个对象, 但其实 Java 中的构造方法, 存在一个问题.

构造方法的名字固定就是类名, 而有的类, 需要有多种不同的构造方式, 但是构造方法名字又是固定的, 就只能使用方法重载的方式来实现了 (方法名相同, 参数个数和类型不同). 这里我给大家举个例子.

当我们想要描述一个点时, 我们想按照两种方式进行构造, 一种是按照笛卡尔坐标构造(提供 x, y), 一种是按照极坐标系构造 (提供距离坐标原点距离 r, 以及点与原点连线和 x 轴形成的角度 a), 这两种构造方式, 参数的个数和类型是一样的, 就无法构成重载.

class Point {
    public Point(double x, double y) {}
   
    public Point(double r, double a) {}
}

方法名相同, 参数个数和类型也相同, 无法构成重载.

此时就可以使用工厂模式来解决以上问题, 不使用构造方法, 而是使用普通的方法来构造对象, 这样方法名字就可以是任意的了. 在普通方法内部, 再来 new 对象, 要注意, 这里的普通方法目的是为了创建出对象来, 所以工厂方法一般都得是静态的.

// 工厂模式
class Point {
	// 工厂方法
	public static makePointXY(double x, double y) { 
		// new进行实例化对象
	}
	// 工厂方法
	public static makePointRA(double r, double a) {
		// new进行实例化对象
	}
} 

2.2 Executors 创建线程池的几种方式

我们继续来讲解标准库中的线程池, Executors 主要有以下几种创建的方法.

  • newFixedThreadPool(): 创建一个固定线程数的线程池.
  • newCachedThreadPool(): 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor(): 创建只包含单个线程的线程池.
  • newScheduledThreadPool(): 设定延迟时间后执行命令,或者定期执行命令. 可以看成是进阶版的定时器 Timer.

其实 Executors 本质上是对 ThreadPoolExecutor 类进行的封装. ThreadPoolExecutor 提供了更多的可选参数 (接口更加丰富), 可以进一步细化线程池行为的设定, 更好的满足开发时的实际需求.

2.3 ThreadPoolExecutor 构造方法中的几个参数

这个是 ThreadPoolExecutor 类中参数最全的一个构造方法.
在这里插入图片描述

  • int corePoolSize , int maximumPoolSize: 前者代表核心线程数, 后者代表最大线程数. ThreadPoolExecutor 里面的线程个数, 并非是固定不变的, 会根据当前任务的情况自适应动态变化. 核心线程数表示, 至少得有这些线程, 即使线程池中一点任务也没有. 而最大线程数则表示, 最多不能超过这些线程, 即使线程池中任务已经很多了, 忙不过来了, 也不能比这个数目多. 这样可以做到, 既能保证繁忙的时候可以高效处理任务, 又能保证空闲的时候不会浪费多余资源.
  • long keepAliveTime , TimeUnit unit: 前者表示当没有任务时, 允许线程空闲的最大时间, 空闲时间超过指定值, 线程就可以被销毁了. 后者表示该空闲等待时间的单位.
  • BlockingQueue workQueue: 线程池内部有很多任务, 这些任务, 可以使用一个阻塞队列来管理. 线程池可以内置阻塞队列, 也可以自己手动指定一个.
  • ThreadFactory threadFactory: 工厂模式, 通过这个工厂类来创建线程.
  • RejectedExecutionHandler handler: 拒绝方式 / 拒绝策略, 是线程池考察的重点. 当线程池中阻塞队列满了之后, 在继续添加任务时, 该如何应对.

2.3.1 RejectedExecutionHandler handler 的几个拒绝策略

  • AbortPolicy: 直接抛出异常, 线程池中所有任务都不执行了.
  • CallerRunsPolicy: 谁是添加这个新的任务的线程, 谁就去执行这个任务.
  • DiscardOldestPolicy: 丢弃掉最早的任务, 执行新的任务.
  • DiscardPolicy: 将添加的这个新的任务直接丢弃.

上面我们谈到的线程池, 一组是被封装过的 (Executors), 一组是原生的 (ThreadPoolExecutor), 在开发过程中, 用哪个都可以, 主要是看公司要求, 以及实际需求.

3. 自己实现一个线程池

  • 核心操作为 submit, 将任务加入线程池中.
  • 使用一个 BlockingQueue 组织所有的任务.
  • 创建 n 个线程, 每个线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            t.start();
        }
    }
}

总结

✨ 本文主要讲解了什么是线程池, 使用了标准库中的线程池, 简单聊了工厂模式, 以及线程池中的几个参数, 最后自己实现了一个线程池.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.

再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!

你可能感兴趣的:(多线程学习之路,python,java,开发语言,线程池,多线程)