(JavaEE)(多线程案例)线程池 (简单介绍了工厂模式)(含经典面试题ThreadPoolExector构造方法)

目录

线程池

池 

为什么,从池子里取的效率比新创建线程效率高?

内核态 和  用户态 

Java标准库中的线程池

线程池的创建 

 工厂模式

工厂模式的作用? 

工厂模式实践 

不同的几种线程池 

第一种: 

第二种 :

第三种:

第四种 :

 ThreadPoolExector 的使用方式

注册任务(简单):submit 

⁜⁜ 构造方法⁜⁜【经典面试题】

​编辑

 实现一个简单的线程池


 

线程诞生的意义,是因为进程的创建/销毁,太重了(比较慢),虽然和进程比,线程更快了,但是如果进一步提高线程创建销毁的频率,线程的开销就不能忽视了。

这时候我们就要找一些其他的办法了。

有两种典型的办法可以进一步提高这里的效率:

1: 协程 (轻量级线程,相比于线程,把系统调度的过程给省略了,变成由程序员手工调度)

(当下,一种比较流行的并发编程的手段,但是在Java圈子里,协程还不够流行,GO和Python用的比较多)

2:线程池(Java用的)

接下来我们就来介绍一些线程池 

线程池

优化频繁创建销毁线程的场景 

首先,我们先来了解一下什么是池:

池 

假设一个美女,有很多人追,美女就从这些人里面挑了一个她最喜欢的交往,交往一段时间之后,美女她腻了,她想换一个男朋友,那她接下来就要干两件事

1:想办法和这个男的分手,需要一些技巧,比如挑他毛病,作 之类的

2:再找一个小哥哥,培养感情,然后交往 

但是这样的效率就比较低啦,有没有什么办法来提高一些效率呢?

当然有,只有美女在和前一个交往的时候,和另一个或多个小哥哥,搞暧昧(养🐟),把他们都放到自己的鱼塘里,那和上一个分手,下一个男朋友来的就很快了。 

我们还是只做了两步,只是把第二步提前了。 

“鱼塘”里的这些人,我们通常叫他们 —— “备胎” 

那这个“鱼塘” 就 可以看成 “池” ,来放“备胎”

同样的,线程池,就是在使用第一个线程的时候,提前把 2,3,4,5...(多个)线程创建好(相对于前面的培养感情),那后续我们想使用新的线程的时候,就不必重新创建了,直接拿里的线程用就行了。(此时创建线程的开销就被降低了)

为什么,从池子里取的效率比新创建线程效率高?

这是因为,从池子里取 这个动作,是存粹的 用户态 的操作,而创建新的线程,这个动作,则是需要 用户态 + 内核态 相互配合完成的操作。

内核态 和  用户态 

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

操作系统,是由 内核 + 配套的应用程序 构成的,

内核:系统最核心的部分

创建线程操作,就需要调用系统 api,进入到内核中,按照内核态的方式来完成一系列动作。

内核态的操作要比 纯用户态的操作开销要更大 :至于为什么,我们来举一个例子解释一下:

银行办业务的例子 

首先这个来办理业务的人他不能 进入柜台后面,只能在大厅里,

这个人想来办张银行卡,需要身份证复印件,但是这个人他忘带了,那此时柜台的服务人员就给了他两个选择:

1:把身份证给她,她去帮他复印

2:大厅的角落,有一个自助复印机,他可以去那里自己复印 

那这两个选择中的第二个,自己复印就是纯 用户态操作(这个人可以立即去复印,完事后立即回来办理业务,整个过程非常利落,非常可控

但是如果交给 柜台的服务人员(第一个选择),这个过程就涉及到 内核态 操作了,那此时,你把东西交给他俩,你也不知道柜员消失之后去做了那些事情,也不是的她啥时候回来,整个过程是不可控的。

操作系统内核,是要给所有的进程提供服务的,当你要创建线程的时候,内核虽然会帮你做,但是做的过程中难免也要做一些其他的事情。那在你这边的结果,就不是那么可控。

上述就是内核态 和 用户态的区别 。

Java标准库中的线程池
 

线程池的创建 

我们发现了,线程池这个对象不是我们直接 new 的,而是通过一个专门的方法,返回了一个线程池的对象。 

这种写法就涉及到了 “工厂模式”(校招常考的设计模式)(和上一篇介绍的 单例模式 并列) 

 工厂模式

工厂模式的作用? 

通常我们创建对象 都是使用 new,new 关键字就会触发 类的构造方法,但是构造方法,存在一定的局限性。

“工厂模式” 就是给 构造方法填坑的。 

 那 “工厂模式” 具体是填的什么 坑 呢,我们举一个例子:

 假设 考虑 一个类,来表示平面上的点

然后我们给这个类提供构造方法:

第一个构造方法: 

期待使用笛卡尔坐标系来构造对象。 

 

第二个构造方法:

使用极坐标来构造对象 

但是编译失败了。 

 

 原因:

很多时候,我们希望构造一个对象,可以有多种构造方式 。那多种方式,我们就需要使用多个版本的构造方法来分别实现,但是构造方法要求方法的名字必须是类名,不同的构造方法 只能通过 重载 的方式来区分了,而重载又要求 参数类型 或 个数 不同。

而上面的两个构造方法 很明显没有构成 重载,当然会编译失败。 

这就是 构造方法的局限性 。

“工厂模式”就能解决上述问题 :

使用普通的方法,代替构造方法完成初始化工作,普通的方法就可以使用方法的名字来区分了。也就不受 重载的规则制约了。

工厂模式实践 

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

伪代码 

class PointFactory {
    public static Point makePointByXY(double x, double y) {
        Point point = new Point();
        point.setX(x);
        point.setY(y);
        return p;
    }
    public static Point makePointByRA(double r, double a) {
        //和上边类似
    }
} 

class Demo {
    public static void main(String[] args) {
        //使用 Point p = PointFactory.makePointByXY(10,20); 
    }
}

上述介绍之后,我们就知道了为啥 线程池 的 对象我们不直接 new 了

 

这种方法就是 工厂模式 

不同的几种线程池 

第一种: 

此时构造出的线程池对象,有一个基本特点,线程数目是能够动态适应的。

cached: 缓存,用过之后不着急释放,先留着以备下次使用。

也就是说,随着往线程池里添加任务,这个线程池中的线程会根据需要自动被创建出来,创建出来之后也不会着急销毁,会在池子里保留一定的时间,以备随时再使用。

 

除了上边的线程池,我们还有其他的线程池:

第二种 :

这个方法就需要我们指定 创建几个线程,线程个数是固定的 (Fix:固定)

第三种:

只有单个线程的线程池: 

第四种 :

类似于 定时器, 只是 不是只有一个 扫描线程 负责执行任务了,而是有多个线程执行时间到的任务.

 第一种和第二种常用

上述这几个工厂方法生成的线程池,本质上都是对 一个类进行的封装 ——  ThreadPoolExector

ThreadPoolExector 这个类的功能十分丰富,它提供了很多参数,标准库中上述的几个工厂方法,其实就是给这个类填写了不同的参数来构造线程池。 

 ThreadPoolExector 的使用方式

ThreadPoolExector 的核心方法:

1.构造方法

2.注册任务(添加任务)

注册任务(简单):submit 

⁜⁜ 构造方法⁜⁜【经典面试题】

构造方法中的参数,很多,且重要, 

我们打开Java文档     Overview (Java Platform SE 8 ) (oracle.com)

打开这个包  juc —— 这个包里放的试和 “并发编程” 相关的内容(Java中,并发编程最主要的体现形式就是多线程)

点进这个包然后往下找: 

然后我们直接翻到构造方法 :

上面的四个构造方法,都差不多,就是参数个数 不一样,第四个 参数最多,能够涵盖上述的三个版本。 

所有我们重点看第四个构造方法: 

 

这一组参数,描述了线程池中,线程的数目: 

这个线程池里的线程 的数目试可以动态变化的,

变化的范围就是【corePoolSize, maximumPoolSize】

那 “核心线程”  和 “最大线程” 如何理解呢?

如果把一个线程池,理解为一个公司,此时,公司里有两类员工

        1.正式员工

        2.实习生

那正式员工的数目,就是核心线程数,正式员工 + 实习生的数目就是最大线程数

正式员工和实习生的区别:

正式员工,允许摸鱼,不会因为摸鱼被公司开除,有劳动法罩着。

但是实习生,不允许摸鱼,如果这段时间任务多了,此时,就可以多搞几个实习生去干活,如果过段时间任务少了,并且这样的状态还持续了一定时间,那空闲的实习生就可以裁掉了。

这样做,既可以满足效率的要求,又可以,避免过多的系统开销 。

ps: 

 使用线程池,需要设置线程的数目,数目设置多少合适?

 一定不是一个具体的数字!!!因为在接触到实际的项目代码之前,这个数目是无法确定的!!!

一个线程 执行的代码,主要有两类:

1.cpu 密集型:代码里主要的逻辑是在进行 算术运算/逻辑判断。

2.IO 密集型:代码里主要进行的是IO操作。

—— 假设一个线程的所有代码都是 cpu 密集型代码,这个时候,线程池的数量就不应该超过N,就算设置的比N大,此时也无法提高效率,因为cpu吃满了。

—— 假设一个线程的所有代码都是 IO 密集型代码,这个时候不吃cpu,此时设置的线程数,就可以是超过N,(一个核心可以通过调度的方式来并发执行)

上述,我们就知道了,代码不同,线程池的线程数目设置就不同,我们无法知道一个代码,具体多少内容是cpu密集,多少内容是IO密集。所以我们无法确定 数目设置多少合适。

正确做法:使用实验的方式,对程序进行性能测试,测试的过程中尝试修改不同的线程池的线程数目,看那种情况,更符合要求。

这一组参数,描述了允许实习生摸鱼的时间,(实习生不是 一摸鱼就马上被开除)

 

这个参数的意思是 阻塞队列 ,用来存放线程池里的任务。

可以根据需要,灵活设置这里的队列是啥,比如需要优先级, 就可以设置 PriorityBlockingQueue

如果不需要 优先级,并且任务数目是相对恒定的,可以使用 ArayyBlockingQueue,如果不需要优先级,并且任务数目变动比较大,就可以用 LinkedBlockingQueue

这个参数就是 工厂模式的体现 ,此处使用 ThreadFactory 作为 工厂类 由这个类负责创建线程

 

使用工厂类来创建线程,主要是为了在创建线程的过程中,对线程的属性做出一些设置。 

如果手动创建线程,就得手动设置这些属性,就比较麻烦,使用工厂方法封装一下,就更方便。 

下面这个参数是最重要的  ,是线程池的拒绝策略

一个线程池,能容纳的任务数量,有上限,当持续往线程池里添加任务的时候,一旦达到了上限,还继续添加,会出现什么效果?

拒绝策略就是来解决这个问题的: 不同的拒绝策略有不同的效果。

 上面的这四个就是不同的拒绝策略

如果队列满了,再添加就直接抛出异常 

新添加的任务,由添加任务的线程负责执行 

 

丢弃最老的任务 

丢弃当前新加的任务 

 实现一个简单的线程池

这个代码比较简单,就不多说了,代码里都有注释 

import java.awt.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: iiiiiihuang
 */
public class ThreadPool {
    //任务阻塞队列
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(4);
    
    //通过这个方法,把任务添加到队列中
    public void submit(Runnable runnable) throws InterruptedException {
        //此处的拒绝策略,相当于第五种策略,阻塞等待(下策)
        queue.put(runnable);
    }
    //构造方法
    public ThreadPool(int n) {
        //创建出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();
        }
    }
}

关注,点赞,评论,收藏,支持一下╰(*°▽°*)╯╰(*°▽°*)╯

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值