并发编程 可见性、有序性、原子性的解决
本文将介绍 Java 对可见性、有序性、原子性的解决。
一、可见性、有序性的解决
1. 解决思路
导致可见性的原因是缓存,导致有序性的原因是编译优化。
因此,合理的解决思路是按需禁用缓存和编译优化。
2. volatile 关键字
具体请看:
3. final 关键字
(1) 作用
final 关键字的作用是:说明此变量固定不变。由于变量固定不变,可以任意地使用缓存和编译优化。
(2) final 可能出现的问题
final 并不是万能的,它有两个可能出现的问题:
由于编译优化,final 变量可能会被先赋指针后初始化,从而导致并发线程获取值错误
- 线程 A 初始化 final 变量
- 赋指针
- 线程 B 获取 final 变量,其值错误
- 线程 A 初始化 final 变量
- 初始化
Java1.5 对 final 的编译优化做了约束,避免了这一问题
- 线程 A 初始化 final 变量
对象溢出导致 final 变量可能在未初始化时被访问
1
2
3
4
5
6
7
8
9
10
11class 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
5void 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
14int var;
Thread B = new Thread(()->{
// 此处对共享变量var修改
var = 66;
});
B.start();
// 主线程等待子线程
B.join();
// 等待后,主线程对子线程的操作可见
// 存在var,其值为66
二、原子性的解决
原子性的产生原因是:高级语言命令会被拆分为多个命令,导致它无法原子性执行。
一个思路是:保证资源在一段时间内只能被一方访问,即使命令被拆分,即使执行中发生任务切换,也能够保证命令的原子性。
具体请看:
参考
- Java 并发编程实战