文章目录
并发编程-理论基础
一、前言
很多小伙伴学习并发编程,上来就是看 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