目录
前言:本篇主要介绍一下学习并发编程之前需要弄清的一些知识点和一些概念性的东西,第三部分的线程与进程有点突兀,好像跟上下的内容不怎么搭配,不过我觉得也很重要,但实在不知道该放哪了,见谅😂
一、并发与并发编程的含义
ps:并发和并行的区别。
- 并发:并发是指两个或多个事件在同一时间间隔发生。(一个人给两个孩子喂饭,同时段交替执行)
- 并行:并行是指两个或者多个事件在同一时刻发生。(两个人给两个孩子喂饭,同时刻执行)
1.1 并发
定义:指系统中存在多个独立活动(任务、线程、进程等)同时执行。并且这些活动之间可能相互影响、交互或者竞争共享资源。
当我们谈到并发时,你可以把它想象成手头同时有多个任务需要做。没错,就是你一次可能需要同时处理多个任务一样。(这里的"同时"指的是同一时间段内)
举个例子,假如你是一名外卖小哥,你的工作就是把外卖送到客户手中。在某个时段,你手里可能有多个订单一起送。那么这就是并发,你同时处理了多个任务(送外卖)。
1.2 并发编程
定义:并发编程指编写能够有效利用系统资源、处理多个并发任务的程序。
在送外卖的过程中,你得考虑一些问题,比如,这些外卖地址天南海北都有,怎么安排路线更省事?如果两个订单超时时间有冲突怎么办?如果客户手机打不通怎么办?这些都是并发编程需要解决的问题。(当然,相关外卖app已经都解决了,这里只是假设😁)
解决:我们可以用一些技巧和工具。比如,用地图规划最佳路线,避免走冤枉路。有冲突就跟客户或商家协调沟通,看看能不能晚点送达。手机打不通可以先送其它单,长时间联系不到就退给商家。那么这些技巧和工具就相当于Java中的锁、同步机制和线程通信。
所以通过并发编程,可以更高效的完成多个任务,提高工作效率。
二、为什么需要并发编程
最大的原因当然还是为了提高效率。假如外卖员不用并发的形式送外卖,那么将变成:如果一家店n个不同地方的人点了n份外卖,外卖小哥就得一个一个送,也就是送完一单,就得返回餐厅再取一单送给另一个客户,如此反复,那这效率真是嘎嘎低。。。所以还是并发比较好。
ps:另外,在打算使用并发编程之前,也需要考虑资源限制、死锁、线程上下文切换等带来的一系列问题。例如做菜,大致顺序就是洗菜、切菜、炒菜,总不能洗一下切一下,或者炒一下洗一下吧。这是个串行的任务,得依据特定得顺序去执行。
三、线程与进程
3.1 什么是线程和进程
- 进程:操作系统中资源分配的基本单位。之间相互独立,不能直接访问对方的资源。每个进程可以包含一个或多个线程。
- 线程:操作系统中调度执行的基本单位。同一个进程中的多个线程共享相同的地址空间和内存,可以直接访问进程的资源。
咱接着说外卖小哥送外卖的例子。假如外卖小哥一下接了3单外卖分别送给A、B、C三个客户,规划好路线后就开始去送,那么“三单外卖的配送”就可以看作是一个进程,而你的不同任务(送给A、送给B、送给C)可以看作是不同的线程。是不是有点迷糊?那就再细点:
- 进程:就是外卖小哥送这3单外卖的过程。外卖小哥有自己的工作空间(使用的设备),资源(外卖小哥的交通工具、保温箱)等,独自完成这三单的配送。
- 线程:就是把外卖送给A、B、C客户的三个不同任务。这三个任务分别代表不同的线程。这些线程共享工作空间(使用的设备)和资源(外卖小哥的交通工具、保温箱),它们可以同时进行,相互之间协作完成这次配送的任务。
要是只有一个线程呢?(也就是不用并发),就是上面提到的,那外卖小哥一次就只能接一单,送完一单返回餐厅取下一单。。。
总结起来就是,进程是完成一个整体任务的过程,线程则是在这个过程中执行的不同任务。
3.2 两者之间的区别与联系
其实3.1或多或少都提了几点,下面就总结一下吧。
3.2.1 区别
资源分配:每个进程都有自己的地址空间、内存、文件句柄和其他系统资源,而线程共享相同的地址空间和内存,能够直接访问进程的资源。
创建和销毁开销:创建和销毁进程的开销通常比线程大,因为进程需要分配独立的资源和环境。而线程的创建和销毁的开销相对较小,因为它们共享进程的资源。
通信和同步:线程之间共享进程的资源,因此它们可以更方便地进行通信和共享数据。进程之间的通信和同步需要使用特殊的机制,如管道、消息队列或共享内存。
3.2.2 联系
协作关系:进程是一个独立的执行实体,可以包含一个或多个线程。线程是进程中的执行单元,协同工作,共同完成进程的任务。
并发执行:进程和线程都能实现并发执行。多个进程可以在同一时间内执行不同的任务,多个线程可以在同一进程中并发执行不同的子任务。
并行执行:由于进程和线程具有并发执行的特性,它们都可以并行执行任务,充分利用系统资源来提高效率。
四、并发编程的优势和挑战
4.1 优势
最大的优势就是可以让程序运行的更快(外卖小哥一次处理多笔订单)、提高用户的体验(只要下单就给你响应)、充分利用资源(看外卖小哥一次最多送几个了,毕竟按单给钱)。这个大家应该都能想到,所以下面重点说一说挑战。
4.2 挑战
并不是启动更多的线程就能让程序最大限度地并发执行。如果希望通过多线程执行任务让程序运行得更快,那就得面临很多挑战,比如线程上下文切换的问题、死锁的问题,还有就是硬件和软件的资源限制问题等。
4.2.1 线程上下文切换
概念:上下文切换是指CPU从一个线程到另一个线程时,需要保存当前线程的上下文状态(包括计数器、寄存器、栈指针等),恢复另一个线程的上下文状态,以便下一次恢复执行该线程时能够正确地运行。
例如:就好比外卖小哥在送外卖的时候,找不着订单上面的地址了,于是向路人问道,但问之前呢,外卖小哥得记住这个地址写的哪哪哪,等问到路之后,才能继续送这笔订单。
看起来确实很降效率。以下是减少上下文切换的几种解决办法:
- 减少线程数:可以通过合理的线程池管理来减少线程的创建和销毁,线程数少,上下文切换的次数自然也就少了。(修建外卖柜,方圆xx公里的外卖都存到外卖柜里,客户自己来取)
- 无锁并发编程:可以避免线程因等待锁进入阻塞状态,从而减少上下文切换次数。(有锁状态:为了安全考虑,某小区同时只能让一位外卖小哥进去,那外卖小哥之间就出现了竞争,自然也会发生“上下文切换”。)
- 使用CAS算法:CAS算法可以避免线程的阻塞和唤醒操作,减少上下文切换次数。(cas:还是上面的那个小区,也有安全考虑,但不限制人数,只要核验一下身份证,符合了就放你进去。)
- 协程(JDK 19的虚拟线程):协程是一种用户态线程,切换操作不需要操作系统参与,所以可以避免上下文切换。(emm...这个等搞清楚了再补充吧。)
4.2.2 死锁
定义:指两个或多个进程(或线程)由于竞争共享资源而相互等待对方释放资源,导致所有相关任务都无法继续执行下去的一种状态。简单的说就是线程1持有A,想要B,线程2持有B,想要A,谁也不松手,导致其他任务都无法进行。
例如:丈母娘要求先买房再结婚,但女婿说先结婚再买房。
产生死锁的4个必要条件
- 互斥条件:资源只能被一个进程(或线程)占用。
- 占有且等待:进程(或线程)已经占有了至少一个资源,并且等待其他资源被释放时继续请求额外的资源。
- 不可剥夺:已经被一个进程(或线程)占有的资源,只能由占有者主动释放,不可强行剥夺。
- 循环等待:存在A等B,B等C,C等A情况,形成了一个闭环。
解决办法:想要解决就不让它们四个同时发生就行
- 破坏不可剥夺:设置优先级,优先级高的可以抢夺资源
- 破坏循环等待:这是最常用的方法。如事务1执行顺序:A->B->C,事务2:C->D->A,这种情况就可能导致死锁,即事务1占有A,等待C,而事务2占有C等待A。所以,要避免死锁就需要把事务2改为:A->D->C。
4.2.3 资源限制
在并发编程中,既存在硬件资源限制,也存在软件资源限制。
- 硬件:硬件资源通常与物理硬件相关,如CPU的核心数、内存容量、存储空间、网络带宽等。例如,CPU核心数影响着系统的处理能力;内存容量影响着同时处理大规模数据集的能力。
- 软件:这涉及对计算机系统内的软件资源限制,例如,操作系统对进程、线程数量进行限制,这就可能导致程序无法创建出更多的进程或线程。
那么这两种限制怎么解决呢?
- 对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的及其处理不同的数据。
- 对于软件资源限制,比如使用连接池管理数据库连接、限制并发连接数控制并发访问、合理使用缓存和异步处理模式等等。
五、总结
本篇文章以“外卖小哥送外卖”为例,介绍了并发和并发编程的概念,解释了为什么我们需要并发编程,以及线程和进程的区别。文章最后重点介绍了并发编程所面临的挑战,并给出了一些建议来应对这些挑战。
ps:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。