Java 并发问题

72 阅读3分钟

并发编程bug的源头

  • 可见性: 一个线程对共享变量的修改,另外一个线程能够立刻看到;
  • 原子性;我们把一个或者多个操作在CPU执行的过程中不被中断的特性,称为原子性;
  • 有序性;编译优化带来的有序问题;

为了合理利用CPU资源,操作系统和缓存等做出的贡献;

  • CPU 增加了缓存,以均衡与内存的速度差异;
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

Java内存模型

  • 规范了JVM如何提供按需禁用缓存和编译优化的方法;

Happens-Before规则

前面一个操作的结果对后续操作是可见的。Happens-Before 约束了编译器的优化行为,虽然允许编译器的优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

  • 程序的顺序性规则:这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作;
  • volatile变量规则: 这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作;
  • 传递性:这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C;
  • 管程中锁的规则: 管程是计算机中的同步原语;这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
  • 线程的start规则:这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
  • 线程的join规则:这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作

逸出

在构造函数里面将 this 赋值给了全局变量 global.obj,这就是“逸出”,线程通过 global.obj 读取 x 是有可能读到 0 的。因此我们一定要避免“逸出”。

// 以下代码来源于【参考 1】
final int x;
// 错误的构造函数
public FinalFieldExample() { 
  x = 3;
  y = 4;
  // 此处就是讲 this 逸出,
  global.obj = this;
}

解决原子性操作- 锁技术:synchronized

  • 当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
  • 当修饰非静态方法的时候,锁定的是当前实例对象 this。
class X {
  // 修饰静态方法
  synchronized(X.class) static void bar() {
    // 临界区
  }
}
class X {
  // 修饰非静态方法
  synchronized(this) void foo() {
    // 临界区
  }
}