volatile无法保证共享变量i++线程安全原因

一、i++

先看一下局部变量i++执行流程与原理。

javap -c -l Demo.class对class字节码文件进行反编译生成汇编代码(只列出我们关心的代码):

javap -v 不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。
javap -l 会输出行号和本地变量表信息。
javap -c 会对当前class字节码进行反编译生成汇编代码。

public class Demo {
    public static void main(String[] args) {
        int i = 0;//行数3
        i++;//行数4
    }//行数5
}

Compiled from "Demo.java"
public class Demo {
  public static void main(java.lang.String[]);
    Code:
       0: iconst_0            // 生成整数0
       1: istore_1            // 将整数0赋值给1号存储单元(即变量i)
       2: iinc          1, 1  // 1号存储单元的值+1(此时 i=1)
       5: return              // 返回
    LineNumberTable:
      line 3: 0
      line 4: 2
      line 5: 5
}

面试中常问到的i = i++:

public class Demo {
    public static void main(String[] args) {
        int i = 0;//行数3
        i = i++;//行数4
    }//行数5
}

Compiled from "Demo.java"
public class Demo {
  public static void main(java.lang.String[]);
    Code:
       0: iconst_0            // 生成整数0
       1: istore_1            // 将整数0赋值给1号存储单元(即变量i)
       2: iload_1             // 将1号存储单元的值加载到操作栈(此时 i=0,栈顶值为0)
       3: iinc          1, 1  // 1号存储单元的值+1(此时 i=1,栈顶值为0)
       6: istore_1            // 将操作栈顶的值(0)取出来赋值给1号存储单元(此时 i=0,栈顶值为空)
       7: return              //返回
    LineNumberTable:
      line 3: 0
      line 4: 2
      line 5: 7
}

总结:

1、int i=0 ;分两步:第一步操作数栈中放0;第二步赋值,把操作数栈中的0赋值给局部变量表中的位置1的变量i,同时消除操作数栈中的0。
2、i++; 一步:把局部变量表中的i的值0自增1,变成1。也就是局部变量自增、自减操作都是直接修改变量的值,不经过操作数栈。

3、i = i++;分三步:第一步,先把局部变量表中i的值0取出放入操作数栈中的栈顶;第二步,把局部变量表中的i的值0自增1,变成1;第三步,将操作栈顶的值(0)取出来赋值给1号存储单元i。

图文流程可参考:https://blog.csdn.net/happy_bigqiang/article/details/90414541

二、为啥volatile无法保证共享变量i++线程安全

如果共享变量i++也和局部变量i++的执行流程相同:直接将局部变量中i值自增加1,那么volatile不就能保证多线程数据安全了?众所周知,volatile无法保证数据同步,它只保证可见性。来看看i++的原因:

public class Demo {
    static int i = 0;//行数2
    public static void main(String[] args) {
        i++;//行数4
    }//行数5
}

Compiled from "Demo.java"
public class Demo {
  static int i;
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2   // Field i:I    //获取静态共享变量i的值
       3: iconst_1                           //生成整数1
       4: iadd                               //将i的值与整数1相加
       5: putstatic     #2   // Field i:I    //将相加后的值赋予静态变量i
       8: return
    LineNumberTable:
      line 4: 0    //共享变量i++,包含了0、3、4、5的代码执行
      line 5: 8

  static {};
    Code:
       0: iconst_0
       1: putstatic     #2   // Field i:I
       4: return
    LineNumberTable:
      line 2: 0
}

总结:static共享变量i++:分3步,一.获取变量i的值,二.值加1,三.加1后的值写回i中。伪代码如下:

int temp = i;
temp = temp + 1;
i = temp;

很明显了,原因就是共享变量i++不是原子操作。i = i+1同i++,也不是原子操作。

多线程环境,假设A、B线程同时执行,都执行到了第二步,B线程先执行结束i=1,因为变量i是volatile类型,所以B线程执行结束马上刷新工作线程中i=1到主存,并且通知其它cpu中线程:主存中i的值更新了,使A工作线程中缓存的i失效。如果A线程这时候使用到变量i,就需要去主存重新copy一份副本到自己的工作内存。但是这时候A执行到了temp = temp + 1,已经用临时变量temp记录了之前i的值,不需要再读取i的值了。所以,虽然变量i的值0在A的工作内存中确实失效了,但是值temp仍然是有效的,既然有效,A就会将第三步的结果i=1再次写入主存,覆盖了之前B线程写入的值。这就是为什么volatile无法保证共享变量i++线程安全的原因。

其实,这些都是JMM Java内存模型带来的数据问题:同步性、可见性、原子性,volatile是JDK提供的解决JMM数据可见性的关键字,最终还是由JVM实现volatile内存可见性语义。上面反编译得到的汇编代码就是JVM具体实现流程的体现。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章