第二部分到此也就结束了。这边按照惯例做一下总结。
并发整块内容包括整章,主要还是描述了并发为什么会产生,以及操作系统如何结合CPU硬件去一步步构造一些使用的api来供开发者避免或者解决这些并发问题。
第二部分主要是通过一些思路来介绍的,更加详细的内容推荐美团技术团队的一篇文章,会有一些细节的补充
其实博客总结和通过书籍系统的学习各有利弊。博客会更加专注于罗列知识点和内容。书籍更多的是从问题启发到引导你思考解决方案,让你自己对问题的本质具备更深层次的了解
这个其实也涉及到了很多同学会遇到的一个问题:为什么背过、看过很多,但是稍微过段时间就忘了?并且难以拿来实际解决问题?
- 博客是知其然,看书是知其所以然
- 博客的内容也会参差不齐,如何判断博客本身的对错?
总结
言归正传,提炼一下第二章的核心
并发问题发生的本质,由共享变量(资源)产生,分两类情况:
-
操作本身缺失原子性,导致并发情况下没有原子性,例如,i++
-
操作系统线程调度顺序的不确定性下,导致的执行顺序异常,例如,子线程创建后马上执行并访问了父进程从未初始化的变量
所以,面对并发问题,在我们使用共享变量或者发现一个变量被共享时,一定要审视一下他的并发安全性
处理并发问题,我们主要依靠的是CPU提供的原子操作以及操作系统提供的线程让出cpu的等待以及唤醒功能
- 通过前一个功能,即CPU提供的原子操作,我们可以实现互斥锁,或者原子变量
- 通过后一个功能,我们可以实现线程间的阻塞等待,并且可以优化互斥锁的自旋操作
通过前面讲到的2个基本能力,我们就可以实现2个并发基本工具:锁,以及条件变量
- 前者用于提供互斥,保证某段存在并发问题的代码互斥执行
- 后者用于提供阻塞等待以及唤醒,用于实现线程间的状态同步
通过上面的2个基本工具去解决并发问题,又会遇到新的问题,死锁
死锁主要就产生于,2个线程之间已经互相持有了彼此需要的一个互斥资源,并为另一个被对方持有得资源等待的情况
额外需要注意的一点,在你使用许多api时,api的实现本身可能会获取某些互斥资源。这种场景也导致了很多死锁的发生。
避免死锁的核心思路
- 避免互相持有互斥资源
- 进行有限尝试或者等待(即尝试获取n次或者只等待m秒,失败后直接退出并释放自己持有的互斥资源)
- 使用对应无锁实现(如果存在)
并发问题逻辑大纲
- 多线程下执行下,线程调度+共享变量产生临界代码,有并发问题
- 硬件以及操作系统提供特性以实现并发工具,解决并发问题
- 并发工具在某些场景下会使用产生死锁问题
- 死锁问题的避免以及恢复
额外扩展
Event loop
一种单线程实现并发执行的模型,在处理大量NIO的场景有最好的表现。
内存屏障
书中没写,内存屏障是用于解决CPU的缓存共享问题以及指令重排序问题的,CPU的cache以及store buffer在更新数据后,虽然有同步机制保证,但是无法保证马上写入到内存,此时其他CPU从内存中取到的可能就是旧值。
内存屏障的作用
内存屏障通过确保特定顺序的内存操作来防止处理器乱序执行带来的问题,尤其是在多核或多线程环境下。内存屏障主要通过以下方式保证内存访问的顺序:
- 防止乱序执行: 处理器通常会为了提高效率而乱序执行指令。内存屏障通过强制执行内存操作的顺序来防止这种乱序执行。例如,一个写屏障可以确保在它之前的所有写操作在它之后的所有写操作之前完成。
- 确保缓存一致性: 在多处理器系统中,每个处理器都有自己的缓存。内存屏障可以确保一个处理器的内存操作对于其他处理器是可见的。例如,写屏障可以确保一个处理器的写操作在其他处理器读取到之前完成。
- 保证同步操作的正确性: 内存屏障在实现锁、信号量等同步机制时非常重要。它可以确保临界区内的操作按预期顺序执行,从而避免竞态条件。
简而言之,内存屏障指令保证在指令前的操作全部已经落到了内存,并且防止指令前后的操作被重排序。
伪共享
之前写netty的时候讲过,也是CPU缓存的问题,当多个处理器核在同一缓存行(通常是64字节)上操作不同数据时,导致频繁的缓存一致性的流量风暴,反而影响了缓存的性能。
此时填充一些无用变量通过offset以使得缓存行不在同一行即可。这种现象主要发生在对高并发性能要求较高的数据结构(例如高并发队列)。