这篇文章上次修改于 1782 天前,可能其部分内容已经发生变化,如有疑问可询问作者。 >这些年,我们的 CPU、内存、I/O 设备都在不断迭代,不断朝着更快的方向努力。但是,在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异。为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为:1.CPU 增加了缓存,以均衡与内存的速度差异;2.操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异 3. 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用 一:缓存导致的可见性问题,同一个cpu的缓存可见,但是不同cpu的缓存不那么可见了。这是硬件程序员给软件程序员挖的坑 二:线程切换带来的原子性问题 + 我们潜意识里面觉得 count+=1 这个操作是一个不可分割的整体,就像一个原子一样,线程的切换可以发生在 count+=1 之前,也可以发生在 count+=1 之后,但就是不会发生在中间。 + 我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性 + CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。 三:编译优化带来的有序性问题 + 例如下面的代码: 在 Java 领域一个经典的案例就是利用双重检查创建单例对象,例如下面的代码:在获取实例 getInstance() 的方法中,我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。 ``` public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } } ``` 这看上去一切都很完美,无懈可击,但实际上这个 getInstance() 方法并不完美。问题出在哪里呢?出在 new 操作上,我们以为的 new 操作应该是: 分配一块内存 M; 在内存 M 上初始化 Singleton 对象; 然后 M 的地址赋值给 instance 变量。 但是实际上优化后的执行路径却是这样的: 分配一块内存 M; 将 M 的地址赋值给 instance 变量; 最后在内存 M 上初始化 Singleton 对象。 优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。 如果对instance进行volatile语义声明,就可以禁止指令重排序,避免该情况发生。
没有评论