「JavaEE」多线程案例分析3:线程池

🎇个人主页Ice_Sugar_7
🎇所属专栏JavaEE
🎇欢迎点赞收藏加关注哦!

🍉简介

“池”是一个非常重要的概念,我们之前学的字符串常量池就是一种常量池

在这里插入图片描述

除了常量池,还有数据库连接池、线程池、进程池、内存池……
池有两个作用:

  1. 提前把要用的对象准备好
  2. 对象用完后也不要立即释放,而是先留着,以备下次使用

我们主要来分析线程池
它是存放线程的池,会把要使用的线程提前创建好,用完之后也不会立即释放,而是放回线程池里。这样就可以节省创建及销毁线程的开销

那为什么从线程池里取线程就比向系统申请更高效呢?
因为从池里取,这部分是通过代码实现的,代码是我们自己写的,是纯用户态的(可控的);而向系统申请创建线程是需要内核来完成的,这个过程是不太可控的
就是说你不知道系统在创建线程之前会不会去做别的事儿,它如果有搞别的,那肯定会耽误时间


🍉标准库中的线程池

标准库提供了 ThreadPoolExecutor 这个类,它的构造方法有很多参数

在这里插入图片描述

我们分别来解释一下每个参数的含义

  1. corePoolSize —— 核心线程数
    标准库提供的线程池,它所持有的线程数并非是一成不变的,而是会根据当前任务量,自适应调节线程个数。即如果任务非常多,那就会多搞几个线程;反之则会缩减线程数
    核心线程数就是一个线程池里最少有多少个线程

  2. maximumPoolSize —— 最大线程数
    顾名思义,就是最多能有多少个线程

  3. keepAliveTime —— 保持存活时间
    就是某个线程,它的空闲时间如果超过这个时间阈值,那就会被销毁掉(或者说空闲状态下能存活多久)

  4. unit —— 保持存活时间的时间单位(h、min、s、ms……)

  5. workQueue —— 一个阻塞队列
    和定时器类似,线程池中也可以持有多个任务,所以用一个阻塞队列来存放任务,如果任务有优先级的话,可以用 PriorityBlockingQueue。Runnable 是用来描述任务的主体

  6. threadFactory —— 线程工厂
    首先要解释一下什么是工厂模式
    工厂模式是一种常见的设计模式,通过专门的“工厂类”/“工厂对象”来创建指定的对象
    它本质上是给 Java 语法填坑的。这里的“坑”指的是,方法重载没办法在返回值相同的前提下,重载参数个数相同、类型一样的方法
    而有时候要重载的方法,它们参数列表中的参数个数和类型虽然一样,但是意思不一样

举个例子,现在有一个类 Point,要用它来表示平面直角坐标系中的点,在数学中有两种表达形式:常规的坐标和极坐标

public class Point {
    double x,y;
    double r,a;
    Point(double x,double y) { //用坐标的形式表示
        this.x = x;
        this.y = y;
    }

    Point(double r,double a) { //用极坐标的形式表示
        this.r = r;
        this.a = a;
    }
}

这样写编译器会报错
我们可以把原先两个方法改成静态方法,直接在方法内部创建一个对象并初始化,然后再返回这个对象
这种把创建对象和初始化封装起来,在静态方法内部完成这些操作的过程,想得到对象,就调用这个方法,它就会“加工”出一个对象,这就是工厂模式。代码如下:

public class Point {
    public double x,y;
    public double r,a;
    public static Point pointByXY(double x,double y) { //用坐标的形式表示
        Point p = new Point();
        p.x = x;
        p.y = y;
        return p;
    }

    public static Point pointByRA(double r,double a) { //用极坐标的形式表示
        Point p = new Point();
        p.r = r;
        p.a = a;
        return p;
    }

    public static void main(String[] args) {
        Point p1 = Point.pointByXY(1,2);
        Point p2 = Point.pointByRA(3,4);
    }
}

pointByXY 和 pointByRA 就称为工厂方法
这两个工厂方法是放在 Point 里面,所以 Point 就叫作工厂类

  1. handler —— 拒绝策略
    这个参数是最重要的一个参数
    线程池中的阻塞队列能够容纳的元素是有上限的,如果往队列中添加任务时,任务队列已经满了,那线程池会怎么做呢?
    这就涉及到拒绝策略了

有四种策略
AbortPolicy直接抛出异常。此时原先任务和新任务都不执行
CallerRunsPolicy新的任务由添加任务的线程去执行,就是“谁揽的活谁去干”。此时新任务就不由线程池执行了
DiscardOldestPolicy丢弃最老的任务
DiscardPolicy丢弃最新的任务

🍌线程池的执行流程

  1. 当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行
  2. 如果没有空闲的线程且当前线程池的线程数量小于corePoolSize,就会创建新的线程来执行任务,否则就会将该任务加入到阻塞队列中
  3. 如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾
  4. 如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略

🍉Executors

由于 ThreadPoolExecutor 参数比较多,用起来比较复杂,所以标准库还提供了另一个版本 —— 工厂类 Executors,它是由 ThreadPoolExecutor 封装后得到的,通过 Executors 创建出来的线程池对象,它内部已经把 ThreadPoolExecutor 创建好了并设置了参数

可以看到已经提供了多种现成的线程池
在这里插入图片描述

Q:Executors 和 ThreadPoolExecutor 分别在什么时候使用?
A:如果没什么要求,只是简单使用一下,那就用 Executors;而如果想要高度定制化(diy),那就使用 ThreadPoolExecutor,参数都由自己设置,掌控权在我们手上,这样可以避免一些不可控因素


🍉实现一个简单的线程池

方便起见,我们写一个线程数目固定的线程池

构造方法指定创建多少个线程,并把这些线程创建好。然后要有一个阻塞队列,持有要执行的任务。还要提供一个方法 —— submit,它用来添加新任务

public class MyThreadPool {
    private List<Thread> list = new ArrayList<>();
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);

    public MyThreadPool(int n) {
        for(int i = 0;i < n;i++) {
            Thread t = new Thread(()-> {
                try {
                    while (true) {
                        Runnable runnable = queue.take(); //取出任务并执行
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
            list.add(t);
        }
    }

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

测试一下:

public class TestDemo5 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool executor = new MyThreadPool(4); //4个线程
        for (int i = 0; i < 1000; i++) {
            int n = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务:" + n + " 当前线程为:" + Thread.currentThread());
                }
            });
        }
    }
}

因为这里每次循环都会改变 i 的值,所以不能直接打印语句中不能写 i,而要重新定义一个变量 n,这个 n 在这里就是“事实 final”,可以放到打印语句中
补充:run 是回调函数,它访问外部作用域的变量就是变量捕获,而变量捕获要求变量是常量 or “事实 final”

结果如下:

在这里插入图片描述

因为线程是随机调度的,所以任务序号是无序的

  • 53
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值