并发编程 可见性、有序性、原子性的解决

本文将介绍 Java 对可见性、有序性、原子性的解决。

一、可见性、有序性的解决

1. 解决思路

导致可见性的原因是缓存,导致有序性的原因是编译优化。

因此,合理的解决思路是按需禁用缓存和编译优化。

2. volatile 关键字

具体请看:

并发编程 volatile

3. final 关键字

(1) 作用

final 关键字的作用是:说明此变量固定不变。由于变量固定不变,可以任意地使用缓存和编译优化。

(2) final 可能出现的问题

final 并不是万能的,它有两个可能出现的问题:

  • 由于编译优化,final 变量可能会被先赋指针后初始化,从而导致并发线程获取值错误

    • 线程 A 初始化 final 变量
      • 赋指针
    • 线程 B 获取 final 变量,其值错误
    • 线程 A 初始化 final 变量
      • 初始化

    Java1.5 对 final 的编译优化做了约束,避免了这一问题

  • 对象溢出导致 final 变量可能在未初始化时被访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Test {
    final int x;

    public Test() {
    x = 3;
    global = this;
    }
    }

    // x未初始化,获取错误
    global.x

4. Happens-Before 规则

Happens-Before 的意思是:前面一个操作的结果可以被后续操作看见。

Happens-Before 可以被分为若干个子规则,如下:

  • 顺序性:

    在同一个线程内,程序中前面的语句 Happens-Before 后续语句。

    1
    2
    3
    4
    5
    void fun() {
    int x = 0;

    // 可以看见 x = 0
    }
  • volatile 变量:

    对一个 volatile 变量的写操作 Happens-Before 后续对这个变量的读操作。

  • 传递性:

    Happens-Before 具有传递性,如果 A Happens-Before B,且 B Happens-Before C,则 A Happens-Before C。

  • 锁:

    锁的解锁 Happens-Before 后续对这个锁的加锁。

  • 启动子线程:

    如果主线程启动了子线程,则启动子线程之前的操作 Happens-Before 子线程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 启动子线程之前的操作
    int var;

    Thread B = new Thread(() -> {
    // 可以看见启动子线程之前的操作
    // 存在var,其值为77
    });

    // 启动子线程之前的操作
    var = 77;

    // 主线程启动子线程
    B.start();

    // 启动子线程之后的操作
    var = 101;
  • 等待子线程:

    如果主线程等待子线程执行完成,则子线程的操作 Happens-Before 主线程等待之后。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int var;

    Thread B = new Thread(()->{
    // 此处对共享变量var修改
    var = 66;
    });

    B.start();

    // 主线程等待子线程
    B.join();

    // 等待后,主线程对子线程的操作可见
    // 存在var,其值为66

二、原子性的解决

原子性的产生原因是:高级语言命令会被拆分为多个命令,导致它无法原子性执行。

一个思路是:保证资源在一段时间内只能被一方访问,即使命令被拆分,即使执行中发生任务切换,也能够保证命令的原子性。

具体请看:

并发编程 synchronized

参考

  • Java 并发编程实战