并发编程的三个特性

原子性、可见性、有序性

1、原子性(同生共死)

一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。

在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作。

  • x = 10;(原子性)
  • y = x;(非原子性)
    • 把数据 x 读到工作空间(原子性)
    • 再把 x 赋值给 y(原子性)
  • i++;(非原子性)
    • 读取 i 到工作空间(原子性)
    • 给 i 值 +1(原子性)
    • 刷新结果到主内存(原子性)
  • z = z + 1;(非原子性)
    • 读取 z 到工作空间(原子性)
    • 给 z 值 +1(原子性)
    • 刷新结果到主内存(原子性)

多个原子性操作合并到一起,就不具备原子性

如何保证原子性

  • 通过 synchronized 关键字定义同步代码块或者同步方法保障原子性。
  • 通过 Lock 接口保障原子性。
  • 通过 Atomic 类型保障原子性。


2、可见性(主内存可见)

当一个线程修改了共享变量的值,其他线程能够看到修改的值。

Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。

volatile 变量和普通变量区别

普通变量与 volatile 变量的区别是 volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

如何保证可见性

  • 通过 volatile 关键字标记内存屏障保证可见性。
  • 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性。
  • 通过 Lock 接口保障可见性。
  • 通过 Atomic 类型保障可见性。
  • 通过 final 关键字保障可见性

3、有序性(Happens-before)

即程序执行的顺序按照代码的先后顺序执行。

JVM 存在指令重排,所以存在有序性问题。

如何保证有序性

  • 通过 synchronized 关键字保障有序性。
  • 通过 Lock 接口 保障有序性。
  • 通过 volatile 关键字标记保证有序性。

Happens-before 原则

1. 程序的顺序性规则

一个线程中,按照程序的顺序,前面的操作 happens-before 后续的任何操作。

对于这一点,可能会有疑问。顺序性是指,我们可以按照顺序推演程序的执行结果,但是编译器未必一定会按照这个顺序编译,但是编译器保证结果一定==顺序推演的结果。

2. 锁定原则

也就是后一次加锁必须等前一次解锁

3、volatile 规则

对一个 volatile 变量的写操作,happens-before 后续对这个变量的读操作。

4、传递原则


如果 A happens-before B,B happens-before C,那么 A happens-before C

5、线程 start 规则

主线程A 启动 线程B,线程B 中可以看到主线程启动B之前的操作。也就是 start happens before 线程B 中的操作。

6、线程 join 规则

主线程A 等待 子线程B 完成,当 子线程B 执行完毕后,主线程A 可以看到 线程B 的所有操作。也就是说,子线程B中的任意操作,happens-before join 的返回。