(二)Java并发-可见性、原子性和有序性

TOC
  1. 1. 缓存问题造成的可见性
  2. 2. 原子性
    1. 2.1. 原子性实现原理
      1. 2.1.1. 总线锁定
      2. 2.1.2. 缓存锁定
  3. 3. 有序性
    1. 3.1. 为什么会有重排序

缓存问题造成的可见性

单核CPU的情况下,多个线程使用同一个CPU,线程1读取共享变量A到CPU缓存,修改后对线程2是可见的,不存在一致性问题。一个线程对共享变量的修改对另一个线程可见称为可见性。
image
多核CPU的情况下,线程1和线程2分别操作不同CPU的缓存,线程1对共享变量A的操作对线程2不可见,出现一致性问题。
image
下面这段代码,使用两个线程对num分别加10000次,启动两个线程,结果应该是20000。但是结果是4537。当线程1读取到num是0,线程2读到的值也是0,线程1修改num为2,保存到内存。此时线程2也修改num为2,保存到内存中。线程1和线程2分别对num做加1操作,但是最终结果是2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i < 10000; i++) {
num++;
}
}).start();
new Thread(()->{
for (int i = 0; i < 10000; i++) {
num++;
}
}).start();
System.out.println(num);
}

原子性

原子性就是把事务看成是一个整体,要么完整执行,要么完全不执行,这就是原子性。一般一条语句需要多个CPU指令执行,例如:num++

  • 将num变量从内存加载到CPU寄存器
  • 在寄存器中将num执行+1操作
  • 将结果保存到内存中(缓存机制可能造成num保存到CPU缓存而不是内存中)
    上面步骤中,如果有其他线程操作num,就会造成数据不一致。因此i++ 是非原子性的操作。要保证原子性,Java中可以使用Synchronized或者Lock对i++加锁。

原子性实现原理

处理器使用总线锁定和缓存锁定两个机制保存个复杂内存操作的原子性。

总线锁定

总线锁定就是使用处理器提供的LOCK # 信号,当一个处理器在总线上输出信号时,其他处理器的请求将被阻塞,该处理器可以独占共享内存。

缓存锁定

总线锁定期间,其他处理器也无法和内存通信,开销比较大。缓存锁定会锁定共享变量内存区域的缓存并写回到内存,使用缓存一致性机制保证修改的原子性。同时处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存中的数据在总线上保持一致。缓存锁定仅能锁定一个缓存行,对于夸缓存行的数据,需要使用总线锁定。

有序性

有序性就是指代码执行的顺序,编译器为了优化性能,会改变语句的先后顺序。重排序可以分为三种:

  • 编译器优化重排序
  • 指令级并行的重排序
  • 内存系统的重排序
    Java中可以用Synchronized和volatile保证有序性。

为什么会有重排序

CPU执行指令的速度远超内存访问速度,CPU为了避免内存访问延迟,将指令管道化,尽量重拍管道内指令达到最大化利用CPU。并且CPU内部包含多个执行单元,可以组合进行算数运算,逻辑条件判断和内存操作。每个单元是并行执行的,指令也是并行执行,执行命令的顺序也增加了不确定性。