JVM实验四:内存模型和volatile

1.volatile的出现要解决什么问题?

源代码到最终执行指令过程中,包括了多次的指令重排序

(1)编译器重排序

 1 //优化前
 2 int x = 1;
 3 int y = 2;
 4 int a1 = x * 1;
 5 int b1 = y * 1;
 6 int a2 = x * 2;
 7 int b2 = y * 2;
 8 int a3 = x * 3;
 9 int b3 = y * 3;
10 
11 //优化后
12 int x = 1;
13 int y = 2;
14 int a1 = x * 1;
15 int a2 = x * 2;
16 int a3 = x * 3;
17 int b1 = y * 1;
18 int b2 = y * 2;
19 int b3 = y * 3;
20 
21 优化前的代码:交替的读x、y,会导致寄存器频繁的交替存储x和y,最糟的情况下寄存器要存储3次x和3次y;
22 优化后的代码:让x的一系列操作一块做完,y的一块做完,理想情况下寄存器只需要存储1次x和1次y。

(2)处理器重排序:指令级重排序

1 LDR R1, [R0];//操作1
2 ADD R2, R1, R1;//操作2
3 ADD R3, R4, R4;//操作3
4 
5 处理器在执行时往往会因为一些限制而等待,如访存的地址不在cache中发生miss,这时就需要到内存甚至外存去取,然而内存和外区的读取速度比CPU执行速度慢得多;
6 对于上面这段汇编代码,操作1如果发生cache miss,则需要等待读取内存外存。看看有没有能优先执行的指令,操作2依赖于操作1,不能被优先执行,操作3不依赖1和2,所以能优先执行操作3,所以实际执行顺序是3>1>2。这里打破了程序执行的有序性。

(3)处理器重排序:内存系统重排序

 1 初始化:
 2 a = 0;
 3 b = 0;
 4 
 5 处理器A执行
 6 a = 1; //A1
 7 read(b); //A2
 8 
 9 处理器B执行
10 b = 2; //B1
11 read(a); //B2

理论执行顺序:以处理器A为例,a=1 应该先执行,x=b 后执行,根据内存模型的效果来看a=1需要执行A1和A3,x=b则需要执行A2,所以正确的执行顺序应该是A1-A3-A2

实际执行顺序:因为从A处理器的角度看,a=1和x=b的执行顺序先后并无影响,即A2先于A3执行并无问题。因此实际的执行顺序是A1-A2-A3

A1-B2-A3的过程中,由于内存模型中的本地缓存,导致A线程的写操作无法立即被B线程给看到,打破了可见性

由于处理器有读、写缓存区,写缓存区没有及时刷新到内存,造成其他处理器读到的值不是最新的,使得处理器执行的读写操作与内存上反应出的顺序不一致

(4)重排序的影响

不论哪种重排序都可能造成共享变量中线程间不可见,这会改变程序运行结果。所以需要禁止对那些要求可见的共享变量重排序。

 

2.volatile如何解决重排序问题

(1)阻止编译重排序

禁止编译器在某些时候重排序

 (2)阻止指令重排序和内存系统重排序(使用内存屏障或Lock前缀指令)

  • 在每个volatile写操作的前面插入一个StoreStore屏障;
  • 在每个volatile写操作的后面插入一个StoreLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadStore屏障。
  1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后。
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前。
  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

在volatile中,所插入的内存屏障将不允许 volatile 字段写操作之前的内存访问被重排序至其之后;也将不允许 volatile 字段读操作之后的内存访问被重排序至其之前。

3.volatile为何无法保证原子性

volatile保证了有序性可见性。在未添加volatile之前,写操作包含了写缓存-写内存这两步。在添加了volatile之后,写操作是一个单独的操作,不可分割。所以记住一点,volatile保证的是多线程下面读写操作这个粒度的原子性,例如下图中添加了锁的set和get方法。

 但volatile无法保证多线程下粒度更大的原子操作,例如i++

i++包括了读内存、自加、写内存三个步骤,但是这三个操作的原子性无法保证

4.实验---指令重排序

 

 1 public class Barrier {
 2     int a = 0;
 3     int b = 0;
 4     int x = 0;
 5     int y = 0;
 6     private static ExecutorService executorService=Executors.newSingleThreadExecutor();
 7     private static ExecutorService executorService1=Executors.newSingleThreadExecutor();
 8     private static ExecutorService executorService2=Executors.newSingleThreadExecutor();
 9 
10     public static void main(String ... args) throws InterruptedException {
11         for (int i=0;i< 1000000;i++){
12             //初始化:
13 Barrier barrier=new Barrier();
14             //处理器A执行
15             executorService1.submit(()->{
16                 barrier.a = 1; //A1
17 barrier.x = barrier.b; //A2
18                 print(barrier);
19             });
20 
21             //处理器B执行
22             executorService2.submit(()->{
23                 barrier.b = 2; //B1
24 barrier.y = barrier.a; //B2
25                 print(barrier);
26 
27             });
28         }
29 
30     }
31     public static void print(Barrier barrier){
32         executorService.submit(()->{
33             if(barrier.x==0&&barrier.y==0){
34                 System.out.println(String.format("=======>%s,%s,%s,%s",barrier.a,barrier.b,barrier.x,barrier.y));
35             }else {
36                 System.out.println(String.format("%s,%s,%s,%s",barrier.a,barrier.b,barrier.x,barrier.y));
37             }
38         });
39     }
40 }

posted @ 2021-01-20 13:08  kozz  阅读(146)  评论(0)    收藏  举报