当前位置: 首页 > news >正文

多线程(一) --- 线程的基础知识

目录

          一.线程是如何做到减少额外开销的

           二.进程和线程的区别

           三.如何创建线程

3.1 继承Thread类,重写run方法

 3.2 实现Runnable接口,重写run方法

 3.3 继承Thread类,重写run方法,但是使用匿名内部类

 3.4 实现Runnable接口,重写run方法,但是使用匿名内部类

 3.5 [常用/推荐] 使用lambda表达式

           四.小结 


     上次写的博客中提到了,在有些场景下,需要频繁的创建和销毁进程的时候使用多进程编程,系统开销会很大。

就拿早期编写一个服务器程序来说:

互联网发展的早期,“网站//web开发”这件事就开始出现了。最早的web开发是使用C语言来编写服务器程序(基于一种CGI(基于多进程的编程模式)这样的技术)。

这样一来,服务器同一时刻会收到很多请求。针对每个请求,都会创建出一个进程给这个请求提供一定的服务,并返回对应的响应。一旦这个请求处理完了,这个进程就要销毁了。

如果请求很多,那么就意味着你的服务器需要不停的创建新的进程,也不停的销毁旧的进程。这样的操作,开销是比较大的。

开销比较大,其中最关键的原因是资源的申请和释放。我们都知道,进程是资源(CPU,硬盘,内存,网络带宽...)分配的基本单位。所以一个进程刚刚启动的时候,首当其冲,就是需要把依赖的代码和数据,从磁盘加载到内存中。而从系统分配一个内存,并非是一件容易事。

           因此,线程就是解决上述问题的方案。线程也可以称为是“轻量级进程”,它是在进程的基础上做出了改进。它的好处在于:保持了独立调度执行,同时省去“分配资源”“释放资源”带来的额外开销。接下来,我会从“线程是如何减少额外开销的”、“进程和线程的区别”,“怎么创建线程”等方面来详细讲解多线程。

          一.线程是如何做到减少额外开销的

           前面提到了,会用PCB来描述一个进程。现在,也使用PCB来描述一个线程。

PCB中有个属性,是内存指针。多个线程PCB的内存指针,指向的是同一个内存空间。这就意味着,只创建第一个线程的时候需要从系统分配资源,后续的线程就不必分配,直接共用前面的那份资源就可以了

除了内存之外,文件描述符表(操作硬盘)这个东西也是多个线程共用一份的,相当于是“共享经济”。

然而,也不是随便搞两个线程,就能资源共享。我们把能够进行资源共享的这些线程,分成组,称为“线程组”。换句话讲,线程组也是进程的一部分。

由图可见,一个进程有一个PCB。但是实际上,一个进程可以有多个PCB,意味着这个进程包含了一个线程组,也就是包含了多个线程。

           总之,每个进程,都可以包含一个线程/多个线程。

 

如图所示,多个线程指向了同一块内存空间。这一块内存空间里存放的是一些代码(指令)数据,它包含了所有线程依赖的所有数据和代码。这些线程可能是各取所需,也可能是有一定公共的。

就算代码数据量很大,也是共享关系。也就意味着,创建第一个线程申请的这些资源,后续的第二个第三个...线程,就不必重新申请了。这样一来,就能够降低频繁申请释放带来的额外开销。

           二.进程和线程的区别

             在有线程之前,进程需要扮演两个角色(资源分配的基本单位,也是调度执行的基本单位)。有了线程之后,就把这两个角色给分开了。此时,进程专注于资源分配,而线程负责调度执行了。

为了让大家能更清楚地理解进程与线程的关系/区别,我在这里举个栗子🌰

假设一个房间里,有一个滑稽老铁吃100 只鸡。这个事情消耗的时间,是比较多的。

那么如何加快效率呢?有这样一种方案:搞两个房间,再搞两个滑稽老铁。每个滑稽老铁吃50只鸡,这样一来,速度一定会大幅增加。

这其实就相当于是多进程方案,但是创建新的进程就需要申请更多的资源(房间和桌子)。所以,另一个方案就是使用多线程。

也就是说,房间和桌子没有增加,但是吃鸡的滑稽老铁多了一个。此时,有两个滑稽老铁,一人吃50只鸡,仍然能够提高效率。并且,这种方案资源开销更小。

那么让我们思考一下,有两个滑稽能够提高效率,如果引入更多的滑稽,吃鸡的速度还会提升吗?换句话说,引入的滑稽是越多越好吗?

答案是“当然不是”!当引入的线程达到一定数量之后,再尝试引入新的线程,效率就没有办法提升了。从图中直观看出,就是桌子坐不下了。

而且,当线程数量太多的时候,线程之间就会相互竞争CPU的资源了(CPU核心数是有限的)。这样非但不会提高效率,反而还会增加调度的开销。

多线程还有一个重要问题,就是线程之间可能会“打架”。

如图所示,这里的1号滑稽和2号滑稽,假如看上了同一个鸡大腿,同时伸手去拿,谁能拿到?这里存在诸多变数。

一旦线程之间起了冲突,就可能会导致代码中出现一些逻辑上的错误(线程安全问题)。

多线程还有一个问题,就是共享资源时,也会有副作用。

一个线程如果抛出异常,并且没有处理好,就可能会导致整个进程被终止。

还是拿1号和2号抢鸡大腿来说。假如1号抢到了,2号没抢到,那么这时2号就很生气。2号说:不让我吃,大家都别吃了!(乌鸦附体)

            我在这里来简单小结一下进程与线程之间的关系/区别。第一,进程是包含线程的。第二,每个线程也是一个独立的执行流,可以执行一些代码,并且单独参与到CPU的调度之中。第三,每个进程有自己的资源,进程中的线程可以共享这一份资源(内存空间和文件描述符表等)。进程是资源分配的基本单位,线程是执行调度的基本单位。第四,进程和进程之间不会相互影响,但如果同一个进程中的某个线程抛出异常,可能会影响到其他线程,从而会把整个进程中的所有线程都异常终止(掀桌)。第五,同一个进程中的线程之间可能会相互干扰,引起线程安全问题。第六,线程也不是越多越好,最好要适当。如果线程太多了,调度开销可能会非常明显。

           三.如何创建线程

             线程的基础知识了解的差不多之后,我们再来谈谈如何创建线程。Java中创建线程的方法有很多种,但其实本质上是一样的。

3.1 继承Thread类,重写run方法

我们知道,写代码的时候,可以使用多进程并发编程,也可以使用多线程并发编程。在Java中不太推荐多进程并发编程,因为很多和多进程编程相关的API,在Java标准库中都没有提供。但是系统提供了多线程编程的API,Java标准库把这些API封装了,在代码中就可以使用了。

Java提供的API,就有Thread这样的类。

我们先创建一个类,然后让它继承Thread类。

接着,我们需要在刚刚创建好的类中重写run方法。所谓run方法,就类似于main方法,是一个Java进程的入口方法。一个进程中至少会有一个线程,而这个进程中的第一个线程,也就称之为主线程。main方法,也就是主线程的入口方法。

此处的run方法,不需要我们程序员手动调用,会在合适的时机(也就是线程创建好了之后),被JVM自动调用执行。

接着在main方法中创建线程的实例,线程的实例才是真正的线程。想要让线程执行起来,还需要调用start方法。

我们需要注意的是,在调用Thread的start方法时,才会真正调用系统API,在系统内核中创建出线程。虽然没有手动调用run方法,但是run方法还是执行了。 

 3.2 实现Runnable接口,重写run方法

Runnable可以理解成“可执行的”,通过这个接口,就可以抽象地表示出一段可以被其他实体来执行的代码。

其中重写的run方法就是Runnable要表示的这一段代码。

如图所示,第一行代码只是一段可以执行的代码,还需要搭配Thread类,才能真正在系统中创建出线程。这种写法,其实就是把线程和要执行的任务进行“解耦合”了

 3.3 继承Thread类,重写run方法,但是使用匿名内部类

所谓的匿名内部类,其实就是一个在一个类里面定义的类,没有名字。这就意味着这个类不能重复使用,用一次就扔了。

写{}的意思是要定义一个类。与此同时,新的这个类继承自Thread。此处{}中可以定义子类的属性和方法,最主要的目的就是重写run方法。

而且,这里的t指向的实例,并非是单纯的Thread,而是Thread的子类(但是我们不知道这个子类叫什么,因为是匿名的)。

 3.4 实现Runnable接口,重写run方法,但是使用匿名内部类

该写法的样式如下图所示:

其中,在Thread构造方法的参数中,填写了Runnable匿名内部类的实例

 3.5 [常用/推荐] 使用lambda表达式

由于方法不能脱离类而单独存在,这就导致为了设置回调函数(run),不得不套上一层类了。因此,上述几种方法都不常用,最常用的还是lambda表达式。

其实,lambda在主流语言中都有,只不过不一定都叫lambda。在C++,Python中是叫做lambda,但在JS,GO中,直接叫做匿名函数了。

lambda表达式创建线程的方式如下图所示:

()中是形参列表,这里能带参数,但线程的入口不需要参数。比如使用lambda代替Comparator,可以带上两个参数,这个是最简洁的代码了。此外,()前面应该还有一个函数名,此处作为匿名函数,就没有名字了。

            上述这些写法都是等价的,都是可以相互转换的。

           四.小结 

             以上,就是我对Java中多线程基础知识的介绍。在下期博客中,我会接着讲解一些关于Thread类的相关属性和用法。觉得博主写的不错的,记得一键三连哦!

http://www.dtcms.com/a/284349.html

相关文章:

  • 使用位运算优化 Vue.js 应用:高效状态管理技巧
  • Oracle 19.28 RU 升级最佳实践指南
  • 装饰器模式及优化
  • 大模型Agent应用开发实战:从框架选型到行业落地
  • 十六进制与嵌入式系统及通信系统
  • yolo8+ASR+NLP+TTS(视觉语音助手)
  • 基于Rust Softplus 函数实践方法
  • 【通识】网络的基础知识
  • 学习日志预告
  • 【测试100问】为什么要做接口测试?
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | GoodCheapFast(Good - Cheap - Fast三选二开关)
  • 区块链之Casper协议背景、演变发展、运作机制和潜在风险
  • 周志华《机器学习导论》第8章 集成学习 Ensemble Learning
  • 2025开源组件安全工具推荐OpenSCA
  • LVS(Linux virtual server)
  • AWS Lambda 最佳实践:构建高效无服务器应用的完整指南
  • 多维动态规划题解——最长公共子序列【LeetCode】记忆化搜索翻译成递推
  • CCS缺陷|冻干/灌装10大缺陷暴露无菌生产系统性漏洞:气流流型缺陷
  • 【android bluetooth 协议分析 03】【蓝牙扫描详解 3】【Bluetooth 中 EIR、IR、BLE 普通广播与扩展广播详解】
  • 数仓建设中,系统数据录入错误或者延迟,如何对历史数据修复或补入?
  • 安装物理机ubuntu系统
  • 初试Spring AI实现聊天功能
  • PyCharm 高效入门指南(引言 + 核心模块详解)
  • ubuntu22 npm install electron --save-dev 失败
  • 基于Rust游戏引擎实践(Game)
  • 智能体开发工具链全景图:IDE、调试器与监控平台
  • 营业执照识别-营业执照文字识别API-营业执照真伪
  • Flutter状态管理篇之ChangeNotifier(一)
  • 什么是协变(Covariant)与逆变(Contravariant)?
  • 在 Windows Server RDS 上配置用户配置文件磁盘查找对应的用户名