一、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具体实现流程的体现。