并发编程导致的问题
导致有序性的原因是编译优化
这个怎么说
比如下面的例子 双重检查锁的单例模式
public class Singleton{
private static Singleton singleton;
private Singleton(){
}
public static Singleton getSingle(){
if(singleton==null){
synchronized(Singleton.class){
if(singleton==null){
// Single 1
this.singleton=new Singleton();
}
}
}
}
}
上面的代码如果多个线程并发执行 是存在问题的
这是因为this.single=new Single(); 翻译成汇编指令 将执行三部
- 申请开闭内存空间M
- 在内存M上初始化 Single()对象
- 将新开辟的地址赋值给Single
但是编译优化使代码发生了重排序
原本 1 2 3的执行顺序 变成了 1 3 2的执行顺序
那如果有两个线程A和B并发执行 getSingle()方法
线程A this.single=new Single(); 时cpu执行权被线程B抢去
该语句不是原子的 会被翻译为 汇编指令分三步执行 执行顺序 为 1 3 2 在执行完第3处时被线程B抢去
线程B执行第一个if语句发现 singleton不为空
直接返回 注意此时返回的是尚未初始化的Java对象
导致可见性的原因是缓存
CPU内部有块高速缓存的区域 如果有个变量A 处理器会优先通过解码内存地址直接访问高速缓存
如果在缓存中找到就成为命中 则直接读取数据
如果未命中 在从主内存中加载 并存入相应的缓存行 但是这个过程 会导致处理器停顿而不能执行其他指令
简而言之 高速缓存会对程序中访问的每个变量保留一份相应的内存空间
另外
cpu和内存之间存在较大的速度差异
为了解决这个问题 在每个cpu中引入了缓存的概念
但是不同CPU之间的缓存时不可见的
这就导致如果多个线程 操作同一个变量 可能某个线程的修改就会被擦除
切换线程会带来原子性问题
由于IO太慢 IO怎么慢了 如果一个进程进行一个IO操作 例如读文件 这个时候该进程可以把自己标记为"休眠"并让出CPU的使用权 待文件读取内存 操作系统会把这个休眠的进程唤醒 唤醒后的进程就有机会重新获得CPU的使用权 在等待IO时会释放CPU使用权 是为了让CPU在这段等待时间里可以做别的事情 这样CPU的使用率就提高了
如果这个时候另外一个进程也读文件 读文件的操作就会排队磁盘驱动在完成一个进程的读操作后,发现有排队的任务,就会立即启动下一个读操作
在高级语言中一条语句往往需要多条CPU指令完成 但是任务切换发生的最小单元确是某个Cpu指令
count+=1;翻译为汇编则至少要三条指令
- 需要把变量 count 从内存加载到 CPU 的寄存器
- 在寄存器中执行 +1 操作
- 将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)
任务切换发生的最小单元是某个CPU指令