并发编程 | 理论基础 - 掌握并发编程的钥匙

总览

本章节的思维导图如下所示:
在这里插入图片描述

引言

并发编程已经成为现代计算中的重要组成部分,特别是在多核和分布式系统日益普及的现代环境中。然而,虽然并发编程带来了诸多的好处,比如性能提升,任务处理的效率增加,但同时它也带来了许多新的挑战,比如数据的同步问题,死锁和竞态条件等。理解并发编程的基础理论,对于开发者来说,就像打开了解决这些问题的钥匙。


为了让你有更清晰的认识,我画了一张图,你可以看一下:
在这里插入图片描述

图中展示的是一个城市道路及其周边设施,汽车在十字路口穿梭疾驰。现在,暂停一下,让我们探讨这样一个问题:假设你是交管部门的小卡拉米,你应该如何操作才能使汽车达到最高的通过率?

可能你的第一反应是:利用交通信号灯。事实上,你还会考虑到将这个策略进行细化,例如在早晚高峰期把绿灯时间动态延长,或者在车流量较小的深夜,启用闪烁的黄灯,让司机根据情况判断是否安全通过。

然而,这种思考方式源自于21世纪的我们。在并发编程的世界中,他们的思考方式可能有所不同。他们可能会说:我们用 Semaphore。这就像交通信号灯控制道路交通一样,Semaphore 控制线程的访问权限。智能交通灯系统可以根据车流量动态调整信号灯,同样,Semaphore 也可以动态调整可访问的线程数。

看,虽然我们所处的科技发展环境有所不同,但是我们的解决问题的方法论却有惊人的相似之处。

然而,道路交通的优化绝非如此简单。让我们看看经验丰富的交管部门老手是如何说的:

他们会提出以下策略:

  1. 实行道路分级:通过对城市道路进行分类,如主干道、次干道、支路等,根据道路的重要性和车流量设定不同的行驶速度上限。
  2. 推广公共交通:鼓励使用公共交通以减少私家车的数量,从而提高整体的道路通行能力。提高公交、地铁的服务质量和班次频率,同时提供优惠政策以吸引更多的市民使用公共交通工具。
  3. 优化路面设计:在十字路口设置专用的左转道和右转道,可以有效减少交叉冲突,提高交通流量。另外,设置合理的行车道宽度,以保证各类车辆可以顺利通行。
  4. 建立完善的交通信息系统:通过安装道路监控摄像头和传感器,实时监控路况并及时处理交通事故,防止交通拥堵。还可以通过移动应用程序提供实时路况信息,以帮助驾驶员选择最佳路线。

回到并发编程的世界中,我们的老手们也有类似的建议:

  1. 实行道路分级:我们可以根据线程的重要性赋予它不同的优先级,这样在竞争资源时,高优先级的线程有更高的获取资源的可能性。
  2. 推广公共交通:线程池的使用可以看作是公共交通的推广。线程池可以有效控制线程的数量,避免过多线程导致的资源浪费。就像公交车能高效地搭载多个乘客(任务)一样。
  3. 优化路面设计:这可以看作是对锁的优化。例如,使用读写锁(ReadWriteLock)可以使多个读线程同时访问资源,但在写线程访问时,所有其他线程(包括读线程)都不能访问,以此提高并发性。
  4. 建立完善的交通信息系统:这可以与 FutureCompletableFuture 类比。这些工具可以处理并发任务的结果,例如监视任务的状态,获取结果等。就像交通信息系统可以帮助驾驶员了解当前路况,选择最佳路线一样,FutureCompletableFuture 可以帮助程序员掌握并发任务的执行情况,得到最优解。

在这两个看似迥异的领域里,我们找到了相似的策略,也发现了共通的智慧。

这个时候,一位从21世纪穿越到并发编程世界的帅气的男子一脸懵逼,为了不暴露穿越的身份,开始努力的学习并发编程基础知识....

基础 | 理解并发并发编程相关的理论知识


并发与并行

并发就像城市的交通系统,允许许多人(线程)同时在路上,但每个时刻只有部分人在移动(并发)。而当城市建设了多条高速公路,许多人可以同时在不同的路上移动,那就是并行。

我们来看下并发编程世界官方教材怎么定义的:

定义

并发(Concurrency):并发是指一个时间段内可以处理多个任务,但在任意时刻点,只有一个任务在运行,即任务之间交替执行。
并行(Parallelism):并行是指在同一时刻能够处理多个任务,即多个任务在同一时刻进行。

关于时间和任务

并发关注的是任务的交错执行,多个任务在一段时间内都有执行的片段,并且交替进行,这使得在宏观上看起来像是这些任务在同时进行。而并行则是指真正的同时执行,即在某一时刻,有多个任务在进行。


进程和线程

你可以想象每个人(线程)都在自己的车里(进程)驾驶。车内的设备和资源(如GPS,燃料)是不能与其他车共享的,就像进程内的资源是独立的。而每个车里可以坐多个人,他们可以相互交流,就像一个进程可以包含多个线程。

我们来看下并发编程世界官方教材怎么定义的:

定义

进程:进程是操作系统进行资源分配和调度的基本单位,是系统中的并发执行的单位。每个进程都有自己的一套独立的用户空间,这个空间中包含了这个进程运行所需要的所有资源(比如:代码,数据,打开的文件等)。进程间的通讯(Inter Process Communication,IPC)需要操作系统提供明确的支持。

线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以有很多线程,每个线程运行自己的代码,但是它们可以共享相同的数据。线程之间的通信比进程之间的通信要容易得多,因为它们共享更多的资源。

关于资源分配

每个进程都有自己独立的内存空间,而一个进程内的线程共享该进程的内存空间。这意味着在一个进程中的线程可以直接读写同一进程中的其他线程的数据,而进程之间的通信(IPC)则需要使用一些特殊的技术。

上下文切换

线程相比进程进行上下文切换的开销更小。因为进程切换需要更多的CPU时间,因此在需要大量进行上下文切换的地方使用多线程会比多进程更有效率。

使用场景

根据应用的需求,可能会选择使用多线程或者多进程。如果任务需要大量的计算而且没有太多的I/O操作,可能会选择多线程,因为它们可以共享内存,而不需要进行频繁的上下文切换。如果任务主要是I/O操作,可能会选择多进程,因为进程之间的独立性可以让每个进程独立地进行I/O操作,不会互相干扰。


除了上面两个重要的概念,我花了很大的篇幅去介绍,接下来我们看下并发编程世界中还有哪些值得我们去关注的。

  1. 线程安全:在城市道路上行驶,必须遵守交通规则,否则可能会发生事故。这就像在并发编程中,我们需要保证线程安全,防止数据竞争或者其他并发问题。

  2. 竞态条件:当两辆车试图同时进入同一个车道,就可能发生碰撞。这就像并发编程中的竞态条件,我们需要通过同步机制来避免。例如由交管部门制定规则,像图中的信号灯就是有效的手段

  3. 死锁、活锁与饥饿:如果四辆车在十字路口相遇,每辆车都在等待其他车先行,就可能导致死锁。如果两辆车在狭窄的道路相遇,反复让对方先行,可能导致活锁。如果某些车一直在等待空闲的停车位,但总是被其他车抢先一步,就可能导致饥饿。

  4. 同步与异步:当你在一个红绿灯处等待时,直到红绿灯变绿你才能前进,这就是同步。而你(主线程)可以将自己的车交给代驾(创建线程),让他们帮你驾驶,然后你可以去做其他事情,这就是异步。

  5. 阻塞与非阻塞:在你等待红绿灯变绿的时候,你无法做其他事情,这就是阻塞。而在等待的时候,如果你可以选择离开,去做其他的事情,这就是非阻塞。

  6. 等待-通知机制:假设车中坐着四个人,你们要轮流跑高速,每进入一个服务区就通知其它三个人,替换自己开车。在自己开车的过程中其它三个人可以闭着眼睛休息(阻塞),也可以玩手机(非阻塞)

  7. JVM中的线程模型:JVM就像城市的交通管理部门,它负责规划道路、指挥交通,确保每个人都可以安全有效地到达目的地。

原来,并发编程的世界和我21世纪的世界差不多嘛,好像还挺有趣的,我得继续学习。


进阶 | 识别并发世界的难题

当然,解决问题之前得发现并界定问题,这两个顺序可不能颠倒。我们来看下有哪些问题:

  1. 交通堵塞(阻塞):这可能是由于大量的车辆试图在同一时间通过同一条道路。在并发编程中,这可以类比为大量线程试图访问同一资源,导致系统性能下降。

  2. 交通事故(数据竞争):当两辆或更多的车辆试图同时进入同一车道时,可能会发生交通事故。在并发编程中,这被称为数据竞争,当多个线程同时访问和修改共享数据时可能会发生。

  3. 交通规则不清晰或不一致(缺乏同步):如果一个城市的交通规则不清晰或者不一致,可能会导致混乱和事故。同样,如果并发程序没有正确的同步机制,也可能导致类似的问题。

  4. 车辆维修(线程中断和恢复):如果一辆车在行驶过程中需要进行维修,那么需要有一种机制能够安全地将车辆移出交通流,并在维修完成后恢复行驶。在并发编程中,线程可能需要被中断和恢复,这需要正确的机制来处理。

  5. 城市规划(资源分配):一个城市需要正确的规划来确保交通流畅。类似地,一个并发系统需要正确的资源分配策略,以确保所有的线程都能有效地执行。

  6. 交通指挥(调度):一个城市的交通系统需要有效的交通指挥来保证交通顺畅。在并发系统中,这就类似于线程的调度。

  7. 十字路口死锁(死锁):在城市道路上,如果每个十字路口的车辆都在等待其他车辆先行,可能会导致整个交通系统陷入停顿,这就是死锁。

  8. 资源饥饿(饥饿):在城市交通中,如果一些路段总是拥堵,导致一些车辆无法通过,这就类似于并发编程中的资源饥饿问题。

  9. 过度优化(优先级反转):有时,为了提高效率,我们可能过度优化某些部分,反而会造成效率下降。在并发编程中,这就类似于优先级反转的问题。

  10. 仓库(内存)和便利店(CPU缓存)缓存不一致:假如仓库中有一件热销产品(共享变量),它很受欢迎,各个便利店(CPU的缓存)可能会决定将它们的一部分存货搬到自己的店里,以便能更快地为顾客提供服务。这个过程就像将内存中的共享变量拷贝到CPU的缓存中。这样,当有顾客(执行指令)需要这种商品(共享变量)时,便利店(CPU的缓存)可以立即提供,而无需耗费时间去仓库(内存)中取货。这大大提高了服务效率(执行速度)。
    但是,这也带来了一种可能的问题。那就是如果仓库(内存)中的商品价格改变了,那么便利店(CPU的缓存)中的商品价格也需要相应地改变。这个过程就是缓存的一致性更新。在并发编程中,如果没有正确处理这个问题,可能会导致严重的数据一致性问题。


进阶 | 解决并发世界的难题

既然发现问题了,就要想办法解决,这有利于形成一套方法论。这样,在并发编程的世界中也能谋个一官半职。我们来看下有哪些:

  1. 交通堵塞(阻塞):我们可以增加更多的道路和高速公路,或者优化交通流量,如引入公共交通、骑行、步行等替代出行方式。在并发编程中,这可以对应于使用更多的线程、进程或者优化资源使用。
  2. 交通事故(数据竞争):引入更严格的交通规则,例如红绿灯、优先级规则等。在并发编程中,这对应于使用互斥锁、读写锁、信号量等同步机制。
  3. 交通规则不清晰或不一致(缺乏同步):清晰明确的交通规则和标志可以帮助避免混乱和事故。在并发编程中,我们需要确保清晰的线程同步机制。
  4. 车辆维修(线程中断和恢复):设置专门的路边服务区,当车辆需要维修时,可以安全地从主路上移出。在并发编程中,对应于正确处理线程的中断和恢复。
  5. 城市规划(资源分配):我们需要一个合理的城市规划,包括公路、公共设施等。在并发编程中,这意味着我们需要合理地分配和管理资源。
  6. 交通指挥(调度):引入交通指挥中心,可以迅速响应交通状况变化,及时调整交通流量。在并发编程中,这对应于线程调度策略。
  7. 十字路口死锁(死锁):设置智能交通系统,如智能红绿灯或交通指挥系统,可以预防和解决死锁问题。在并发编程中,对应于使用特定的算法或协议,如银行家算法,以避免死锁。
  8. 资源饥饿(饥饿):通过合理的交通规划和资源分配,保证所有的车辆都能得到公平的服务。在并发编程中,这可以通过合理的线程调度和资源分配策略来实现。
  9. 过度优化(优先级反转):在交通规划中,我们应避免过度优化某个地方,而忽视了其他地方的需求。在并发编程中,对应于避免优先级反转问题,可以使用优先级继承或优先级上限等策略。
  10. 仓库(内存)和便利店(CPU缓存)缓存不一致:引进先进的仓库管理系统,当仓库中的商品价格改变时,这个系统会立即通知所有的便利店。在收到通知后,便利店就会立即更新自己的商品价格。同时,如果便利店发现自己的商品价格与仓库不同步,就会主动向仓库询问最新的价格,并立即进行更新。
    另外,我们还可以引入一些限制和约定,以防止数据不一致的问题。例如,我们可以规定,只有当所有便利店都确认已经更新了商品价格后,仓库才能进行下一次价格更改。这样,就能确保在任何时候,所有便利店的商品价格都是一致的。要解决这个问题,关键在于实现一个有效的通信和协调机制,以保证在并发操作中,所有参与者都能看到一致的数据。

常见面试题


解释并发编程和并行编程的区别

并发编程是指一个时间段内可以处理多个任务,但在任意时刻点,只有一个任务在运行,即任务之间交替执行。而并行编程是指在同一时刻能够处理多个任务,即多个任务在同一时刻进行。

什么是进程?什么是线程?它们有什么区别?

进程是操作系统进行资源分配和调度的基本单位,是系统中的并发执行的单位。线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以有很多线程,线程共享进程的内存空间。

解释什么是死锁,并给出一个简单的例子

死锁是指两个或更多的进程或线程因争夺资源而造成的一种阻塞的情况,这些进程或线程中的每一个都在等待其他的进程或线程释放它或它们需要的资源。例如,假设有两个线程A和B,A拥有资源1,B拥有资源2,现在A想要获取资源2,B想要获取资源1,他们都在等待对方释放资源,那么就会发生死锁。

什么是竞态条件?如何避免竞态条件?

竞态条件是指在并发环境中,多个线程或进程为了访问或改变共享的数据而竞争,结果取决于线程或进程执行的相对速度。为了避免竞态条件,通常我们会使用各种同步机制,如锁,信号量,条件变量等,以确保在任何时刻只有一个线程或进程能够访问或修改共享的数据。


总结

感谢你看到这里,我们来做下总结。首先,我为你展示了一个并发世界观。把你置身于其中,以一位初学者的角度学习这个世界的运行规律。并且带你理解两个非常重要的概念:“并发与并行”,“进程与线程”。然后用21实际的知识储备来理解这个异世界存在的难题,并且带你把这些问题都解决掉。最后为你除了一些常见的面试题,现在你是否都掌握了呢?


后续

理解线程是并发编程中不可或缺的一部分,这将帮助我们更好地编写高效且可靠的并发代码。敬请期待下一篇文章,我们将一起深入学习Java线程,掌握并发编程的更多知识。在下一篇博客中,我们将专注于Java中的线程。我们将介绍线程的生命周期,如何创建和启动线程,以及如何使用Java中的工具进行线程同步。我们还会探讨线程安全,线程局部变量,以及如何处理线程中的异常。如果你感兴趣,可以继续阅读:并发编程 | 线程-并发世界的主角

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kfaino

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值