CPU、内存、IO设备的速度差异
程序整体的性能取决于最慢的操作 — 读写IO设备
为了合理利用CPU的高性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序做了以下优化:
- CPU增加了缓存,以均衡与内存的速度差异;
- 操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异;
- 编译程序优化指令执行顺序,使得缓存能够更加合理的利用。
并发程序的问题根源
1.缓存导致的可见性问题
单核时代:所有的线程都是在一颗CPU上执行,一个线程对缓存的写,另一个线程一定是可见的。
多核时代:每颗CPU都有自己的缓存,线程A操作CPU-1的缓存,线程B操作CPU-2的缓存,线程A对变量的操作对线程B就不具备可见性了。
2.线程切换带来的原子性问题
一条语句往往需要多条CPU指令完成,例如count+=1,至少需要三条CPU指令。
指令1:首先把变量count从内存加载到CPU的寄存器;
指令2:在寄存器执行+1的操作;
指令3:最后将结果写入内存(缓存机制导致写入的可能是CPU缓存而不是内存)。
操作系统做任务切换,发生在任何一条CPU指令执行完(不是一条语句)。CPU能保证的原子操作是CPU指令级别的,而不是高级语言的操作符。
把一个或多个操作在CPU执行的过程中不被中断的特性称为原子性。
3.编译优化带来的有序性问题
双重检查创建单例对象
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
问题:instance = new Singleton()
我们认为的new操作执行顺序应该是:
- 分配一块内存M;
- 在内存M上初始化Singleton对象;
- 最后将M的地址赋值给instance变量。
实际优化后的执行顺序却是这样的:
- 分配一块内存M;
- 将M的地址赋值给instance变量;
- 最后在内存M上初始化Singleton对象。
优化后导致的问题:线程A先执行getInstance()方法,当执行完指令2时恰好发生了线程切换,切换到了线程B上;若此时线程B执行getInstance()方法,当执行到第一个判断
if (instance == null)时,会发现instance != null,所以会直接返回instance,而此时的instance是没有初始化过的,若访问instance的成员变量会触发空指针异常。
总结:
缓存导致的可见行问题;
线程切换带来的原子性问题;
编译优化触发的有序性问题;