一直以来,因为cpu、内存、I/O存在着巨大的速度差别,cpu>内存>I/O。为了平衡这三者的差别,计算机结构、操做系统、编译器都作出了巨大的贡献,主要体现为:java
1. 增长cpu缓存,以均衡与内存的速度差别缓存
2. 编译器优化指令执行次序,使得缓存可以更加合理利用多线程
3. 操做系统增长进程、线程,以分时复用cpu,进而均衡cpu与I/O的速度差别并发
以上的优化确实提升了cpu的使用率,提高了总体程序的性能,可是也给咱们的并发程序带来了一些问题性能
1. cpu缓存致使的可见性问题优化
在单核时候,只有一个cpu以及一个cpu缓存,不一样的线程对应的是同一个缓存,则不存在可见性问题。在多核时代,每颗cpu都有本身的缓存,不一样的线程可能操做的是不一样的缓存。例如线程 A操做的是cpu1的缓存,而线程B则操做的是cpu2的缓存,很明显这个时候线程A对变量v的操做,对于线程B而言则是不可见的spa
2. 编译优化带来的有序性问题操作系统
编译器为了优化性能,有时候会改变语句的前后顺序,这样不但能够更加合理的利用cpu缓存,同时能够减小cpu没必要要的停顿。而后编译器的优化只能保证串行语义的一致,没法保证多线程的语义也一致。在java中一个经典案例就是利用双锁检查建立单例对象。线程
public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
问题出在new操做上,new操做通过优化后,执行指令的顺序为:指针
1. 分配一块内存M
2. 将M的地址赋值给instance变量
3. 在内存M上初始化instance变量
我么假设A线程执行getInstance方法,执行完指令2后,B线程开始执行,这时B发现第一个instance==null 为false,直接返回了instance实例,然而此时instance实例尚未初始化完成,这个时候B线程访问instance的成员变量就可能触发空指针异常
3. 线程切换带来的原子性问题
线程的切换是由操做系统来处理的,而操做系统走切换是可以发生在任何一条cpu执行完成的。而不少高级语言的一条语句对应cpu指令,count++这个操做则须要三条指令
- 指令1:将count从内存load到cpu寄存器中
- 指令2:将寄存器中的count进行+1
- 指令3:将count写入内存
当线程A执行完指令1后,切换到线程B则发生原子性问题