【JavaEE初阶系列】——多线程案例四——线程池

目录

🚩什么是线程池

🎈从池子中取效率大于新创建线程效率(why)

🚩标准库中的线程池

🎈为什么调用方法而不是直接创建对象

🎈工厂类里的方法

📝newCachedThreadPool()

📝newFixedThreadPool()

🚩实现线程池

🚩ThreadPoolExecutor

🎈ThreadPoolExecutor的构造器参数

🎈ThreadPoolExecutor的新任务拒绝策略


🚩什么是线程池

首先我们想象一个场景就是比较渣的场景,我是一个男生,很帅并且有才华,追问的人呢,很多,排成了一个长队。然后我就挑了一个既好看又有钱又有才华的女生进行交往,交往一段时候后,我腻歪了,想换个女朋友,此时我要做俩个事情:1>想办法分手,2>再找一个小哥哥,培养感情。此时进行这俩个操作的时候,效率是很低的,有没有办法优化呢?

**优化:**我和这个女生A再交往的过程中,同时再和另一个女生B搞暧昧(培养感情),当我想和女生A分手的时候,就只要分手了后我们就可以和女生B直接在一起了。(此时我和女生B感情是有一定的基础了)。此时女生B就是我们所说的备胎。

**进一步优化:**我需要更高的效率的话,更换女朋友,就可以再和女生A在一起的时候,同时和女生B,C,D交往联络感情,此时女生B,C,D都是我的备胎,此时备胎就构成了------备胎池。

所以和线程池有同样的方式,线程池顾名思义就是存放线程的池子,等需要了就直接调用了,省去了启动和销毁的损耗了。

线程池最大的好处就是减少每次启动、销毁线程的损耗


从上面线程池我们知道,等需要了就直接从线程池中取,但是为什么从池子取得效率比新创建线程得效率更高呢?

🎈从池子中取效率大于新创建线程效率(why)

  • 池子中取 ,这个动作,是纯粹用户态得操作
  • 创建新的线程 ,这个动作,是需要用户态+内核态相互配合完成的操作。

如果一段程序,是在系统内核中执行,此时就称为"内核态",如果不是,则称为"用户态".

操作系统,是由 内核+配套 的应用程序构成的,内核则是 系统最核心的部分,创建线程操作,就需要调用系统api,进入内核中,按照内核态的方式来完成一系列操作。

场景:

滑稽老哥去银行存钱,但是需要身份证复印件,但是滑稽老哥没有,所以滑稽老哥就有俩个选择。

  • A:银行柜员说:你可以给身份证给我,我去帮你打印
  • B:银行柜员又说: 大厅的角落,有一个自助复印机,你可以自行去复印。

A这个过程就是涉及到了内核态操作了,所谓内核态就是柜员要进行的操作,此时你交给柜员后,柜员会在给你复印件之前会做哪些工作(因为操作系统内核是给所有的进程提供服务的,当你要创建线程的时候,人家内核会帮你做,但是做的过程,难免会做一些其他的事情)------不可控

B这个过程就是纯粹用户态的操作,所谓用户态就是用户自己要进行的操作,滑稽老哥就可以立即去复印,复印完了之后就立即回来,整个过程中,没有任何拖泥带水的。------可控


🚩标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService

线程池对象不是直接创建出来的,而是通过一个专门的方法,返回一个线程池对象。

ExecutorService service= Executors.newCachedThreadPool();
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中
    Executors.newCachedThreadPool();其实是个工厂模式(设计模式),也就是Executors是个工厂类,需要创建线程,但是为什么调用方法呢?而不是直接创建线程池对象呢?

🎈为什么调用方法而不是直接创建对象

创建对象的同时,new关键字就会触发类的构造方法,但是构造方法,存在一定的局限性。

考虑有个类,我们期待用笛卡尔坐标系,来构造对象,又或者用极坐标构造对象,写在一起的时候,这俩个方法是无法重载的(也就是说在一个类中我们要实现不同方式的初始化),就会编译失败。其实很多时候,构造一个对象,希望有多种构造方式,多种方式,就需要使用多个版本的构造方法来分别实现,但是构造方法要求方法的名字必须是类名,不同的构造方法,就只能通过 重载 的方式区分了。(重载是方法名相同,参数个数类型不同),使用工厂模式/设计模式,就能解决这个问题,使用普通的方法,代替构造方法来完成初始化工作,普通方法就可以通过方法名的不同来进行区分了,不必因为重载的规则而限制了。

通过这种,我们通过一个工厂类Executors调用方法创建不同类型的初始化工作。Executors是工厂类,那么调用的方法是工厂方法,然后加工好之后,返回的是整个加工好的线程,而ExecutorService就是线程池,是由工厂类调用工厂方法创建好的。

实践中,一般单独搞一个类,给这个类搞一些静态方法,由这样静态方法负责构造出对象

class PointFactory{
   public static Point makePointByXY(int x,int y){};
   public static Point makePointByRA(int R,int A){};

等到需要调用哪个的时候,我们就可以通过类来调用方法。


🎈工厂类里的方法

Executors 创建线程池的几种方式
newFixedThreadPool: 创建固定线程数的线程池
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池. 
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer. 
Executors 本质上是 ThreadPoolExecutor 类的封装.

📝newCachedThreadPool()

ExecutorService service= Executors.newCachedThreadPool();

newCachedThreadPool()方法中cached缓存,用过之后不着急释放,先留着以备下次使用(此时构造出的线程池对象,有一个基本特点,线程数目是能够动态适应的)随着往线程池中添加任务,这个线程池中的线程会根据需要自动被创建出来,创建出来之后也不会着急销毁,会在池子里保留一定的时间,以备随时再使用。


📝newFixedThreadPool()

ExecutorService service1=Executors.newFixedThreadPool(4);

固定的,指定创建几个线程。具体需要创建几个线程,正确做法就是使用实验的方式,对程序进行性能测试,测试过程中尝试修改不同的线程池的线程数目,看哪种情况下,最符合你的要求。

还有些工厂方法了解即可。


🚩实现线程池

  • 核心操作为 submit, 将任务加入线程池中
  • 使用 MyThread 类描述一个工作线程. 使用 Runnable 描述一个任务.
  • 使用一个 BlockingQueue 组织所有的任务
  • 每个 t 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
package ThreadPool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class MyThread{
    BlockingQueue<Runnable> queue=new ArrayBlockingQueue<Runnable>(1000);
    public void submit(Runnable runnable){
        queue.offer(runnable);
    }
    public void takeTask(int n){
        for (int i = 0; i < n; i++) {
            Thread t=new Thread(()->{
                try {
                    Runnable runnable=queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
}
public class ThreadPool_test {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        for (int i = 0; i <100; i++) {//一百个任务
            myThread.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("我爱zyf");
                }
            });
        }
        myThread.takeTask(10);//10个线程执行100个任务
    }
}

打印了十个,10个线程执行了10个任务,因为里面没有用while(true)循环,一个线程执行完任务之后就结束了。但是这些线程是可能同时执行各自的任务,但是一个线程肯定是执行一个任务。


🚩ThreadPoolExecutor

在阿里巴巴手册中有一条建议:

【强制】线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

如果经常基于Executors提供的工厂方法创建线程池,很容易忽略线程池内部的实现。特别是拒绝策略,因使用Executors创建线程池时不会传入这个参数,直接采用默认值,所以常常被忽略。

ThreadPoolExecutor可以实现线程池的创建。ThreadPoolExecutor相关类图如下

从类图可以看出,ThreadPoolExecutor最终实现了Executor接口,是线程池创建的真正实现者。


ThreadPoolExecutor核心方法有俩个,一个是构造方法,一个是注册任务(添加方法).

🎈ThreadPoolExecutor的构造器参数

 public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler)
  • 参数一
  • 指定线程池的线程数量(核心线程): corePoolSize,不能小于0;
  • 参数二
  • 指定线程池可支持的最大线程数: maximumPoolSize,最大数量 >= 核心线程数量;
  • 参数三
  • 指定临时线程的最大存活时间: keepAliveTime,不能小于0;(则表示实习生可以摸鱼的时间,并不代表一摸鱼就被开除了)
  • 参数四
  • 指定存活时间的单位(秒、分、时、天): unit,时间单位;
  • 参数五
  • 指定任务队列: workQueue,不能为null;
  • 参数六
  • 指定用哪个线程工厂创建线程: threadFactory,不能为null;
  • 参数七
  • 指定线程忙,任务满的时候,新任务来了怎么办: handler,不能为null;
  • 临时线程触发机制
  • 新任务提交时发现核心线程都被占用,任务队列也满了,但还可以创建临时线程,此时才会创建临时线程。
  • 何时拒绝任务
  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。

**"核心线程"**如何理解呢?

如果把一个线程池,理解成一个公司,此时,公司里有俩类员工,一批是正式员工(有编制的),另一批是实习生(无编制的),正式员工的数目就是核心线程数,最大线程数就是正式员工+实习生。这个线程池里线程的数目是可以动态变化的,变化的范围就是[corePoolSize,maxmumPoolSize],正式员工可以摸鱼,不会因为摸鱼,被公司开除,但是实习生不允许摸鱼,如果这段时间任务多了,就可以多搞几个实习生,来干活,如果过段时间任务少了,并且少的状态持续了一段时间,空闲的实习生就被裁掉了。(这样做,既可以满足效率的要求,又可以避免过多的系统开销)

BlockingQueue<Runnable> workQueue阻塞队列,用来存放线程池中的任务的,可以根据需要灵活设置这里的队列是啥,需要优先级,就可以设置PriorityBlockingQueue,如果不需要优先级,并且任务数目是相对恒定的,可以使用ArrayBlockingQueue,如果不需要优先级,并且任务数目变动较大,就用LinkedBlockingQueue。

ThreadFactory工厂模式的体现,此处使用ThreadFactory作为工厂类,由这个类负责创建线程,使用工厂类创建线程,主要是为了再创建过程中,对线程的属性做出一些设置。(如果手动创建线程,就得手动设置在这些属性,就比较麻烦,使用工厂方法封装一下)

RejectedExecutionHandler线程池的拒绝策略,一个线程池,能容纳的任务数量,是有上限的,当持续往线程池里添加任务的时候,一旦已经达到上限了,继续添加,会出现什么效果呢?(不同的拒绝策略,就有不同的效果)

🎈ThreadPoolExecutor的新任务拒绝策略

就比如一个学校老师,一个星期得上8节课,学校领导找到我,想让我去参加校园活动。

  • 1.听到这个要求的时候,老师心态崩了,心情很烦躁------这属于**(.AbortPolicy直接抛出异常)**
  • 2.老师直接和领导说,她这边有好多课去不了,让领导一个人去参加校园活动**(.CallerRunsPolicy拒绝新任务,由新增任务的线程去执行)**
  • 3.老师给这一周8节课中一节课给割了,然后和领导一起去参加校园活动**(.DiscardOldestPolicy丢弃任务队列中最老的任务,执行新任务去)**
  • 4.老师拒绝了校领导,继续去上课,然后校领导也不去了,这个校园活动都不去参加了。(DisCardPolicy丢弃新加的任务,新加任务的线程也丢弃了)

在面试中,拒绝策略和线程数目是面试的重点。


保持现状。

相关推荐
LuckyLay8 分钟前
Spring学习笔记_27——@EnableLoadTimeWeaving
java·spring boot·spring
向阳121821 分钟前
Dubbo负载均衡
java·运维·负载均衡·dubbo
Gu Gu Study30 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
WaaTong1 小时前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_743048441 小时前
初识Java EE和Spring Boot
java·java-ee
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
小灰灰__1 小时前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭1 小时前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果2 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林2 小时前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac