首页 » 文章 » java 并发编程学习2--- 可见性、原子性和有序性问题:并发编程Bug的源头

这些年,我们的 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语义声明,就可以禁止指令重排序,避免该情况发生。

添加新评论