【Java EE】-多线程编程(八) 多线程案例之线程池&&工厂模式

作者:学Java的冬瓜
博客主页:☀冬瓜的主页🌙
专栏【JavaEE】
分享:纵是百万大军,又如何!——李大帅《画江湖之不良人》
主要内容:什么是线程池,线程池的应用,用普通方法替代构造方法实现工厂模式。线程池标准库的使用,变量捕获,关于线程池的源码分析,ThreadPoolExecutor的构造方法参数分析,线程池拒绝策略。自定义实现线程池。

在这里插入图片描述

在这里插入图片描述

一、线程池

1、什么是线程池

       我们知道,线程池、数据库连接池、字符串常量池等是一种类型的,就是用一个容器,把同类型的东西装进这个容器,便于后面使用,所以叫做"池"。
       那么我们为什么要使用线程池?当线程数量足够多时,我们发现线程申请资源和释放资源的花费时间也会达到一个高度,使我们的程序效率变低,此时有两种方法:1、搞一个比线程更轻量的线程,协程/纤程,go中就有纤程,所以go对并发编程有着优势;2、可以使用线程池,在创建线程池的时候一次创建多个线程对象,待使用线程时,从线程池中取出线程执行任务。
       那么为什么使用线程池要快呢?因为如果不使用线程池,线程对象在应用程序中实例化,并通过操作系统提供的api创建和管理线程(包括销毁);而使用线程池后,线程池中的线程对象不是由操作系统创建的,而是由线程池管理程序在程序运行时动态创建。我们知道应用程序的执行要比操作系统执行快很多。因此当我们需要使用多个线程来执行任务时,线程池就可以很大的提高效率。

2、工厂模式

  • 使用线程池时,用下图的方式创建线程池,调用用Executors的静态方法来创建对象(这里的对象是线程池)。再用ExecutorService接收(后面会分析为什么)。
    Executors这样的类叫做:工厂类
    newFindThreadPool()这样的方法叫做:工厂方法,这里就使用了工厂模式这种设计模式。
    工厂模式:使用普通方法,来代替构造方法,创建对象。
    在这里插入图片描述
  • 那么为什么不使用构造方法?其原因就是构造方法在创建对象时,要创建不同类型的对象,应该写两个构造方法,让这两个方法构成重载,但是有时候可能无法构成重载。
    举一个简单的例子:写一个点的类,用笛卡尔坐标系和极坐标系两种方式构造点对象。我们想想,因为参数类型和个数都一样,两个构造方法无法构成重载,所以会产生冲突。
    如果Executors是使用构造方法构造的线程池,那么它的构造方法也会有这种冲突,但是使用静态方法则可以让名字不同,就很轻易地区分开来。这样工厂类的工厂方法方法就可以无限扩展,而不需要考虑创建对象时出现上述问题。
public class Point{
	// 笛卡尔坐标系构造方法
	public Point(double x, double y) {}
	// 极坐标系构造方法
	public Point(double r, double a) {}
}

3、使用标准库的线程池

1> 多少线程数合适?

  • 只能根据具体的项目来看,看吃了多大CPU,占了多大内存,效率如何等等。
    因为不同的情况需要的线程数不同。
    CPU密集型:因为8核16线程已经全部在跑,那么再申请线程也没啥用,反而可能会降低效率。(这里的8核是物理核心,16个线程是逻辑核心,每个物理核心里面有2个逻辑核心)
    IO密集型:在等待IO时,我可以申请较多线程,等IO结束,就开始运行。
  • 下面代码理解:在下面代码中,我们申请了一个线程个数为10个的线程池,然后有100个任务需要线程池来执行
    使用pool.submit()方法来使用线程池执行任务。
public class Main {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(n);
                }
            });
        }
    }
}

2> 变量捕获

  • 在java1.8之前,变量捕获只能捕获final修饰的变量,在jdk1.8开始,不要求必须final修饰,只要不做修改的变量就可以捕获。
    在下面的代码中,如果去掉第4行代码,第9行打印i,那就会报错。因为i是一直在改变的。i 的生命周期结束时是整个for循环结束,而 n的生命周期是 for循环单次执行内,所以n的值是没有被修改的,因此可以变量捕获。
    在这里插入图片描述

3> 包装类Executor的工厂方法和ThreadPoolExecutor分析

  • 我们在上面已经说了Executor是一个工厂类,提供了下图中的工厂方法,用于创建不同类型的线程池。
    在这里插入图片描述
  • 那么,为什么使用ExecutorService接收线程池对象呢?我们继续往下看。
    在这里插入图片描述
    当我们点进newFixedThreadPool方法后,发现返回的是一个ThreadPoolExecutor对象,其实我们在Executor这个工厂类的工厂方法中的返回值都是ThreadPoolExecutor类型的,只是传入的参数不同,所以最终实现的线程池不一样。
    接下来我们看继承关系。ThreadPoolExecutor继承了AbstractExecutorService这个抽象类,而AbstractExecutorService抽象类实现了ExecutorService接口。因此相当于ThreadPoolExecutor类实现了ExecutorService接口,因此可以使用ExecutorService接收线程池对象。
    在这里插入图片描述

4> ThreadPoolExecutor构造方法参数分析

@ 部分参数分析:
  • 这几个构造方法中最后一个构造方法的参数最多,且包含前面的所以构造方法。我们来分析ThreadPoolExecutor类的最后一个构造方法的的参数分析:
    ThreadPoolExecutor类里的线程分为两类:一类是正式员工(核心线程),一类是零时工(空闲线程)
    1.corePoolSize:核心线程数
    2.maximumPoolSize:总线程数(正式员工和零时工之和就是总线程数)
    3.keepAliveTime和unit:二者一起描述零时工(空闲线程)最大摸鱼时间,keepAliveTime是空闲线程等待工作的最大时间,unit是时间的单位。
    4.BlockingQueue<Runnable> workQueue是使用阻塞队列,因为如果当前没有任务需要执行,那线程池中的线程应当陷入阻塞;或者任务满了,那任务入队会先阻塞。
    5.threadFactory:用于创建线程。
    在这里插入图片描述
@ 线程池拒绝策略参数分析(Hander 重点):
  • Hander描述了线程池的"拒绝策略",即如果当前线程池任务队列满了,可以采取的策略。
    6.Hander有以下四种类型:
    在这里插入图片描述

二、自定义实现线程池

  • 使用阻塞队列来存任务
  • 和定时器的部分一样,使用一个线程来取出队列中的任务元素,然后执行。在这里,任务在线程池中的线程中执行。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

// 自定义线程池
class MyThreadPool{
    BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    // 构造方法,创建线程,且取出任务队列中的任务,在进行执行。
    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                while (true){
                    try {
                        Runnable task = queue.take();
                        task.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
    // 向任务队列添加任务
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

}
// Main测试类
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);
        for (int j = 0; j < 100; j++) {
            int n = j;
            pool.submit(new Runnable() {  // runnable描述每个任务
                @Override
                public void run() {
                    System.out.println(n);
                }
            });
        }
    }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学Java的冬瓜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值