线程池详解
创始人
2024-05-08 07:06:28
0

日升时奋斗,日落时自省

目录

1、线程池解释

2、线程池使用

2.1、代码解析

2.2、创建方式

2.3、ThreadPoolExecutor解析

2.4、拒绝策略

 3、自主实现简单线程池

1、线程池解释

线程池可以从字面意思理解就是一个能装多个线程的池子,是的其实也就是这么理解。

认识他从问题开始 ,为什么要用它,我们已经可以自己实现多线程了,还要它创建是不是多此一举,实则不是的,凡是存在必有道理

了解线程的友友们都知道,线程的出现就是因为进程实现并发编程的时候,太重了(创建和销毁资源开销太大)

这个时候我们还是用了线程来代替进程操作,线程也叫做“轻量级进程” ,创建和销毁线程比创建和销毁进程更高效,此时使用多线程实现并发编程也就更高效了。

但是我们希望趋向于更好的并发编程,较少资源开销,线程上还需要更精进的。

第一种就是 :“轻量级线程” 已经有了,叫做“协程” 也可以叫做“纤程” 但是还没有更进到java标准库中,所以我们还用不了,其他部分语言是有的(java以后会不会有不知道,越更新肯定是越好的)

第二种就是 : 使用线程池 来降低线程创建和销毁的开销的

注:之所以说线程是 轻量级进程 是因为与进程相对比,减小了开销,但是不代表线程真的开销就很小。

需要将事先创建好的线程,放到“池”中,后来需要来再从里面取,用完了再还给池,这样创建和销毁线程就更家高效了。

这里简单的叙述可能一点点的模糊,先理论上简述一下,创建线程和销毁线程都是由操作系统内核完成的,如果从池子里获取和归还给池子,是算自己用户代码就能实现的,不必交给操作系统,因为操作系统慢啊,开销还大,池子更快。(图解一下)

2、线程池使用

2.1、代码解析

在java标准库中,也给咱们提供了现成的线程池,可以直接使用(这里先那一种创建方法进行解释)

ExecutorService pool= Executors.newFixedThreadPool(10);

这里使用ExecutorService来定义接收,线程池数目固定10个,注意这个操作是使用了某个静态方法(newFixedThreadPool),直接构造出一个对象来(这个方法的背后会有new操作,就不需要我们去写了)

这样的方法称为“工厂方法” ,提供这个工厂方法的类,也就是“工厂类”,这个行代码就使用了“工厂模式”(一种设计模式)

是不是感觉这种不用new 的还要搞一个方法来创建对象的有点多此一举,不是哈,是因为构造方法有缺陷,还记得重载吧,构造方法能重载,但是参数类型和个数是不能完全相同的。

 这里也是因为创建线程池使用了种方法调用,所以在这里提一下工厂模式。

2.2、创建方式

创建方式有多种,这里写四种创建方法,其实也都是大体相同。为了创建出来而已

线程池创建后会有任务调度,那就需要一个方法来传这个任务,线程池提供了一个方法submit(任务)可以题给线程池提交若干任务

第一种 刚刚见过的

创建方法(newFixedThreadPool)

 创建一个固定线程 线程数量可控根据不同任务情况 设置线程数量

public static void main1(String[] args) {//创建线程池ExecutorService pool= Executors.newFixedThreadPool(10);for (int i = 0; i < 100; i++) {pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("第一种线程池创建方式");}});}}

第二种

创建方法 (newCachedThreadPool)

适用于短时间大量任务情况,线程数量动态变化,任务多了就多几个线程,如果任务少了,就少些线程

public static void main2(String[] args) {ExecutorService pool=Executors.newCachedThreadPool();for (int i = 0; i < 50; i++) {int n=i;pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("方法二");}});}}

第三种

创建方法(newSingleThreadExecutor)

(1)复用线程 不需要频繁创建销毁 

(2)提供了任务队列 和 拒绝策略

public static void main3(String[] args) {ExecutorService pool=Executors.newSingleThreadExecutor();for (int i = 0; i < 20; i++) {int  n=i;pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("方法三");}});}}

第四种

创建方法(newScheduledThreadPool)

类似于定时器,也是让任务延时执行,只不过执行的时候不是由扫描线程自己执行了,而是由单独的线程池来执行

public static void main(String[] args) {ExecutorService pool=Executors.newScheduledThreadPool(10);for (int i = 0; i < 50; i++) {int n=i;pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("方法六"+n);}});}}

以上所有的创建方法都是通过包装ThreadPoolExecutor来实现出来的

其实这里有一个小问题可以提一下?(就是创建线程池的代码)

为什么在这里要int n=i 一下再打印出来,直接打印i可以吗?

答案:不可以, 因为i是主线程的局部变量(在主线程的栈上)随着主线程的执行结束就会销毁,很可能主线程这里for循环执行完了,当前run的任务在线程池还没有排到呢此时就已经销毁了,那剩下的任务谁做

这里的是n是一个变量捕获 run方法属于Runnable这个方法的执行时机,不是立刻马上执行而是再未来的某个节点执行(线程池里的线程就像是在排队等待,对应到谁了谁去执行任务)

为了避免线程之间生命周期不同,作用域差异,导致后序执行run的时候i已经销毁,于是这里采用了变量捕获,这里相当于run方法把当前主线程的i拷贝了一份,这时的n就是在执行run的一个局部变量

针对捕获变量:在java中,JDK1.8之前,要求变量捕获,只能捕获final修饰的变量,其实就是为了捕获不会改变的变量,在JDK1.8开始,有所更正,要求不一定非得带final关键字,只要满足条件,代码没有修改这个变量也可以进行捕获如当前场景

2.3、ThreadPoolExecutor解析

ThreadPoolExecutor里面有啥呢,有的挺多的,可以在javaAPI8官方文档里面找java.util.concurrent包里面找,可以找到,下面是对该类的参数进行一个解析

 说到线程池的线程数量,其实都是根据情况而定,没有一定的答案,只有尝试才知道那种是最好的,在测试中选择。

不同的程序特点不同,设置线程的数量也是不同的

分为两种极端类型:

(1)CPU密集型: 每个线程要执行的任务都是需要CPU进行一系列的算术运算,此时线程池线程数,最多也不应该超过CPU核数,因为没有CPU就这么多核,设置更大也得空着,线程数多了也没有用不是嘛(这里说的核数是逻辑核心个数 )

注:现在电脑的CPU一般是8核16线程8个物理核心,16个逻辑核心,每个逻辑核心都可以执行一个线程,一个物理核心可以有多个逻辑核心

(2)IO密集型 每个线程干的工作就等待IO(读写硬盘,网卡,等待用户输入等),不吃CPU,此时这样的线程就处于阻塞状态,不参与CPU调度,是不是感觉线程一下不归CPU核心数管了,这不多多益善嘛,说是这么说的,计算机资源有限,所以不太可能,但仍然可以创建多个线程超过核心数

当然了以上提及都是理想化,因为大项目不会就CPU密集型,或者就IO密集型;真实情况,一部分需要CPU,一部分是IO密集型 ,这就取决于是那种类型的更多,CPU密集型多,线程数就少一点,反之线程数就多一点(两种类型相辅相成,要想知道设置多少个线程数为做优,需要针对程序进行测试,测试结果说最优才是最优,没有具体的衡量标准)

2.4、拒绝策略

拒绝策略在java标准库中专门提供了,四种方法下面是一些例子作为解释,拒绝策略像是我们生活中不同拒绝方法。

 3、自主实现简单线程池

实现简单的线程池不会有太大的代码量

思路:

(1)需要一个队列(阻塞队列)(这篇博客中的生产者消费者模型解释了阻塞队列)前面解释过线程池的任务像是排队一样,没有接到任务就会先阻塞这

(2)该类的构造方法中创建n线程并执行任务

(3)写一个submit方法进行提交任务(阻塞队列入队列操作)

以上三点就可以写一个简单的线程池,当然就是简单实现,所以没有拒绝策略等多中方法

自定义一个类 我自定义叫做 MyThreadPool 后面的所有代码都是包括在这个类里面的

首先创建一个阻塞队列

 //此处不涉及到时间  此处只有任务 就直接 使用Runable ,在定时器中是因为还有个是时间,所以需要进行自定义一个类来表示private BlockingQueue queue=new LinkedBlockingQueue<>();

这里可以创建一个对象 原因: 防止后面出现测试的时候,出现对象不一致的问题(后面在解释)

 private  Object locker=new Object();   //直接每次都可以获得这个this 那就是一定的相同

然后写啥,该出创建的都已经创建了,该写这个我们线程池的构造方法

(1)构造方法的参数是线程数

(2)任务如何来如何执行,需要从阻塞队列中出队列

//n 表示线程的数量public MyThreadPool(int n){//构造方法 中 创建线程for (int i = 0; i < n; i++) {Thread t=new Thread(()->{while(true){try {//任务出队列 + 接收任务Runnable runnable=queue.take();//这里做个打印可以看是不是同一个对象System.out.println(locker);//任务执行runnable.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();}}

就剩下最后一步,写一个submit方法 用来提交任务,上面为什么能接收到任务(阻塞队列中为什么有任务),就是因为在该方法中进行任务提交,放入阻塞队列中。

public void submit(Runnable runnable){//其实这里可以理解为获取先线程任务   放到线程池中try {//阻塞队列存放任务queue.put(runnable);System.out.println(locker);} catch (InterruptedException e) {throw new RuntimeException(e);}}

把以上四个代码放在一个我们自定义的类(MyThreadPool)中,就是一个简单的线程池 

现在所有代码都走完了,开始解释为什么要创建一个对象,还要打印出来看是不是相同的对象,这也是因为我看到这样的问题了,所以放到这里写一下。

有友友一定认为什么情况这个对象都是相同的,因为在一个类里面不都一样吗,如果这里没有创建这个对象的话,直接打印this对象,这里也是一样的,是的,但是有友友会在这里习惯性的使用匿名内部类结果就不一样了,匿名内部类搭配this在这里就会导致对象不一致

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...