【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丢弃新加的任务,新加任务的线程也丢弃了)

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


保持现状。

相关推荐
BillKu22 分钟前
Java + Spring Boot + Mybatis 插入数据后,获取自增 id 的方法
java·tomcat·mybatis
全栈凯哥23 分钟前
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
java·算法·leetcode·链表
chxii24 分钟前
12.7Swing控件6 JList
java
全栈凯哥25 分钟前
Java详解LeetCode 热题 100(27):LeetCode 21. 合并两个有序链表(Merge Two Sorted Lists)详解
java·算法·leetcode·链表
YuTaoShao26 分钟前
Java八股文——集合「List篇」
java·开发语言·list
PypYCCcccCc31 分钟前
支付系统架构图
java·网络·金融·系统架构
华科云商xiao徐1 小时前
Java HttpClient实现简单网络爬虫
java·爬虫
扎瓦1 小时前
ThreadLocal 线程变量
java·后端
BillKu1 小时前
Java后端检查空条件查询
java·开发语言
jackson凌1 小时前
【Java学习笔记】String类(重点)
java·笔记·学习