文章目录
并发编程-理论基础
一、前言
很多小伙伴学习并发编程,上来就是看 JUC 包,背面试题,但是对并发编程的底层原理不甚了解,导致写出的程序出现奇怪的问题,也没有足够的理论支撑去排查,说实话,我一开始也是这样的。但是随着学习的深入和面试的经历,我越发了解到系统学习并发编程的重要性。
从今天开始,我就带领小伙伴们,从理论触发,逐渐吃透并发编程
俗话说得好,基础不来,地动山摇,在学习并发编程之前,我们要先了解并发编程解决的原因、其引发非问题
这里要提醒一句,并发编程和 JMM (内存模型)密不可分,小伙伴在学习之前,可以先去复习一下 JMM 的知识哦
二、理论基础
1、为什么需要多线程
众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异(在耗时的 IO 执行的时候,可以切换线程执行其他任务,这是多线程出现的最重要的原因)
这种方式导致原子性问题
- CPU 增加了缓存,以均衡与内存的速度差异(CPU 寄存器,一级缓存,二级缓存…其均衡与内存的差异的主要做法,是先将修改写入缓存中,然后在一定时间后,才会将缓存内容写入内存,这样,可以将多次对内存的修改,打包成一次进行修改,减少对内存总线的占用)
导致 可见性问题
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用
导致有序性问题
2、并发问题的根源
可见性、原子性、有序性
1)可见性
可见性,是由于 CPU 缓存导致的
我们通过下面的例子来看看具体可见性问题的成因
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值
2)原子性
数据库里,我们有 ACID 四个事务的性质,其中 Atomicity,及原子性,它表示的意思是对于同一个事务,在提交之前,其内部的操作要么全部成功,要么全部失败
并发编程中的原子性问题,其实和这个差不多,比如说我们希望一个线程,可以处理几个任务,而不被其他线程打扰
**经典的问题:**写一个方法,模拟账户A 向账户B 转账
最最简单的想法,是这样的:
public boolean transform(Account a,Account b) {
synchronized(a) {
synchronized(b) {
// 执行转账操作
}
}
}
这个写法,有一个致命的问题,就是有可能发生死锁。假如在线程 a 想向线程b 转钱的过程中,线程b 又向线程a 转钱,就可能出现 a 在等待获取资源b,b 在等待获取资源 a 的尴尬局面
有小伙伴可能会说了:"那么我们给方法加一个 synchronized 锁不就好了"
public synchronized boolean transform(Account a,Account b) {
// 执行转账操作
}
这样的操作也有自己的问题,就是其效率是在是太低了,为方法加上 synchronized 锁,那么同一时间内,一次只能有两个对象进行操作,其他的转账操作全部都要排队。假设像支付宝这种级别的支付业务,一次支付可能要排队好几个小时
通过上面两个例子,我们就可以清楚的认识到,并发编程的原子性,要考虑的不是怎么实现原子性,而是怎么保证高效和不会死锁,这些在(包括我们上面举的银行转账的例子),我们会在讲 JUC 的时候,再次提及
3)有序性
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序
重排序后出现问题的例子如下:
// Processor A
a = 1; //A1
x = b; //A2
// Processor B
b = 2; //B1
y = a; //B2
// 初始状态:a = b = 0;处理器允许执行后得到结果:x = y = 0
这段代码,因为重排序的存在,执行结果可能为 x = y = 0
那为什么需要重排序?重排序提高效率又体现在哪里?
通过下图可以发现,经过重排序后,会先将对数据写后的结果,写入缓冲区中,接着,不是按照顺序去刷新主存,而是先去读取主存中的数据,之所以会出现这么荒唐的结果,是因为处理器希望所有缓存对主存的操作一次完成,从而提高效率(IO 永远是程序运行效率的最大瓶颈),而这也就造成了我们程序员的麻烦