并发编程三大特性
# 可见性
我们编写的任何程序,最终都会转成机器码,交给 cpu 执行,由于 cpu 和内存的速度差异巨大,如果 cpu 每执行完一个指令都将数据写回内存,执行下一个指令又从内存读取数据,会严重降低 cpu 的执行效率,所以 cpu 每次都是读取一个缓存行(cache line, 64 字节)数据将数据放在 cpu 缓存中。
如下是 cpu 三级缓存架构示意图
(图片来自网络)
从 cpu 到各计算单元速度参考
计算单元 | 时钟周期 | 时间 |
---|---|---|
寄存器 | 1 cycle | < 1ns |
L1 Cache | 3 - 4 cycle | 1 ns |
L2 Cache | 10 - 20 cycle | 3 ns |
L3 Cache | 40 - 45 cycle | 15 ns |
内存 | 120 - 240 cycle | 80 ns |
扩展
因为有缓存行的存在,有时候可以使用缓存行对齐的写法提高程序性能,关于这方便内容可以自行 google
# 顺序性
为了提高 cpu 执行效率并发执行指令,当两个指令没有先后依赖关系时,会并发执行指令,就可能造成指令执行循序和代码不一致的情况,这种情况只能保证在单线程情况下的执行结果是一致的,而在多线程可能会得到错误的结果。
以下是复现指令重拍情况存在的代码
public class ReorderingDemo {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread one = new Thread(() -> {
shortWait(100000);
a = 1;
x = b;
});
Thread other = new Thread(() -> {
b = 1;
y = a;
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
// 只有线程中的代码出现指令重排,才可能出现这个结果
if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
可见性和指令重拍问题的本质是为了提高 cpu 性能,但是引入一项技术的同时又带来了其他问题。java 内存模型(Java Memory Model,JMM)提供了解决可见性问题的规范:Happens-Before 规则。
具体规则包括:
- 线程内执行的每个操作,都保证 happen-before 后面的操作,这就保证了基本的程序顺序规则,这是开发者在书写程序时的基本约定。
- 对于 volatile 变量,对它的写操作,保证 happen-before 在随后对该变量的读取操作。
- 对于一个锁的解锁操作,保证 happen-before 加锁操作。
- 对象构建完成,保证 happen-before 于 finalizer 的开始动作。
- 甚至是类似线程内部操作的完成,保证 happen-before 其他 Thread.join() 的线程等。
# 原子性
由于操作系统多线程的存在,导致了 cpu 切换带来的数据一致性问题。经典的count += 1
举例,一行代码是拆分了多个 cpu 指令:
- 需要把变量 count 从内存加载到 CPU 的寄存器;
- 在寄存器中执行 +1 操作;
- 将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
cpu 在执行完任意一个指令时都可能切换到其他线程,假如以上count += 1
执行完第二条指令后,切换大其他线程执行,此时的线程读取 count 的值就是一个没有更新完成的数据,用此时的数据进行计算,就会得到错误的记过。过程如下图:
# 参考
CPU多级缓存 (opens new window) Java内存访问重排序的研究 (opens new window)
Last Updated: 2024/04/23, 01:30:37