Java内存模型有三个重要特征:
原子性、
可见性、
有序性
原子性(Atomicity)
原子性是指一个操作是不可中断的,即使是在多个线程一起执行的情况下,一个操作一旦开始执行,就不会受到其他线程的干扰。
Java内存模型定义的8种原子操作,就具有原子性。
以下哪些代码是原子性操作:
- i = 0
- i++
- i = j + 1
需要注意的是:
CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。
上面四个操作中,
- 是原子性
在Java中,对基本数据类型(除 long 和 double 外)、引用变量的读写操作都是原子性操作 - 不是
包含了三个操作:读取i值、i + 1 、将+1结果赋值给i - 不是
包含了三个操作:读取j值、i + 1 、将+1结果赋值给i
因此下段代码,即使看似只有一句代码,在多线程的情况下,也会有并发问题的
static int count = 0;
void test() {
for(int i=0; i<10000; ++i) {
++count;
}
}
++count至少会被拆分成3个指令,在多线程的情况下,CPU可能在这3条指令中发生切换
- 读取count的值;
- count加1;
- 更新count的值。
可见性(Visibility)
一个线程对共享变量的修改,另外一个线程能够立刻看到
在Java内存模型中,聊过主内存是个逻辑概念,硬件上含包含缓存等,内存并不直接与Cpu打交道,而是通过高速缓存与Cpu打交道。cpu <—> 高速缓存 <—> 内存。 所以在单核时代,是不存在可见性问题的。因为每个线程都对应的同一个缓存,一个线程对共享变量有修改,另一个线程可以立即看到: 多核时代,不同的线程可能在不同的CPU核心上工作,不同的CPU核心又都有各自的缓存。
为了减少变量访问的时间消耗,线程可能直接从对应的CPU缓存中读取,而不是从主内存中读取。
这样,一个CPU缓存区中的内容对于其他CPU而言是不可见的。这就导致了在其他CPU上运行的其他线程可能无法看到其他线程对某个变量值的修改。
有序性(Ordering)
指令重排序:当一个线程中的两条代码完全不相干时,那么它们的执行顺序是可以交换的,在多线程情况下,会出现有序性问题
public class VolatileTest2 {
static int x = 0;
static int y = 0;
static int a = 0;
static int b = 0;
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
x = 0;
y = 0;
a = 0;
b = 0;
new Thread(new Runnable() {
@Override
public void run() {
x = a;
b = 1;
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
y = b;
a = 1;
}
}).start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (x == 1 && y == 1) {
System.out.println("指令重排序出现的问题");
}
}
}
}
按照正常逻辑顺序来看这块代码的话,x和y是不可能同时为1的。 因为如果x为1,那么第二个线程的代码肯定执行完了,也就是y = b执行过了,此时b为0,y就一定是0。 反之,y为1,x = a已经执行过了,此时a为0,x就一定是0。 但结果会:出现重排序问题
我们以第一个线程为例子: x = a 和 b = 1 没有关联性,那么它们的执行顺序就不一定是代码所写的顺序,如果我们写成:a = 1 和 x = a ,那么它们会按照代码顺序执行。 但是在多线程中,由于抢占式线程调度,从而造成乱序。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。
重排序分三种类型:
- 编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 - 指令级并行的重排序
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 - 内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
|