为什么会出现并发问题,可能是因为这些年,存储设备和Cpu的不断发展
Cpu的速度已经到了一种可怕的速度,整个主机之中,Io设备 Cpu 内存三者的关系可以为
Cpu一秒,内存一分钟,内存一秒,Io一分钟
为了满足水桶效应,即Io和内存可以跟上Cpu的速度,则做了以下的优化
1.Cpu增加了缓冲,用来均衡差异
2.增加了线程和进程,来分时使用Cpu,均衡差异
3.编译程序优化了指令执行次序,使得缓冲能够合理的运用
故,并发问题的源头,在于此
CPU增加了缓存导致的可见性问题
何为可见性:
一个线程对于共享变量的修改,另一个线程能够立刻看到,称之为可见性
在单核的时代,所有的线程都在一颗CPU上执行,故在此CPU上,一个线程的执行必然是对自己是可见的
而且只要是单核的状态下,无论多少个线程操作其中的缓存,得到的都是CPU最新的值(因为不存在CPU之间的值同步)
在多核的时代,每个CPU都有自己的缓存,这时候对于可见性的保证就比较困难了
多个线程在不同的CPU上执行,操作的是不同的CPU的缓存
这就带来了多核情况下的可见性问题
增加了线程,导致了线程切换带来的原子性问题
无论在计算的什么时代中,都有着多进程的功能,也就是一遍写着代码,一遍听着歌
这是因为操作系统允许某个进程执行一小段时间,执行完成,再进行切换,这个时间称为时间片
比如说一个场景,一个进程要去执行IO操作,这时候就会把自己标为休眠状态,让其他线程进行执行,
自己去执行IO工作,这样能保证CPU的利用率到达近乎百分之百
这就需要一个队列,保存了接下来Cpu执行的工作,从而达到这个Cpu线程执行完成了,下个线程补上的情况
在Java中常见的并发编程问题就出在线程切换的时候
为什么会怎么说
何必如一个CPU指令 count+=1,实际的CPU指令为
count从内存加载到CPU中
count+1
count将值写入内存或者缓存中
在操作系统执行过程中,可能发生在任何一个CPU指令中,就是上面说的任何一个汇总
故,执行两次count+=1,可能会出现结果为1的情况
故,经常把一个或者多个操作在Cpu执行过程中不被中断的特性称为原子性,Cpu保证Cpu指令的操作是原子性的
但是我们实际需要保证的原子性需要建立在高级语言层面
指令优化,带来的编译优化有序性问题
在Java中,通常会存在自带的程序优化,即为 程序汇总 a=6,b=7,编译器优化后可能变成 b=7 a=6,虽然不会影响到结果,
但在并发编程中,可能导致意想不到的bug
举一个简单的例子
通常来说一个对象的创建,在理论上来说应该为
分配一块内存M
M上初始化对象
将M和变量挂上联系
初始化完成
但是编译器优化过后,会变为
分配一块内存空间M
将M和变量进行挂上联系
M上初始对象
初始化完成
这就出现了一个bug问题,即为在并发获取实例的时候,可能会出现获取到的对象是一个null,
这是获取到的对象可能只是挂上了联系,并没有初始化对象
虽然缓存,线程切换,编译优化都是为了更好的服务开发者,但是使用一个新的技术必然会带来对应的新的问题,需要开发人员更加的小心