一、局部变量i++
先看一下局部变量i++执行流程与原理。
javap -c -l Demo.class 对class字节码文件进行反汇编生成可读的jvm字节码文件:
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 // 将栈顶值赋予局部变量表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 // 将栈顶值赋予局部变量表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。(很明显可发现jvm的赋值操作是基于操作数栈的,这也是为什么说jvm的执行引擎是基于栈的,这里的栈就是操作数栈)
图文流程可参考:https://blog.csdn.net/happy_bigqiang/article/details/90414541
二、为啥volatile无法保证共享变量i++线程安全
一些概念说明:
JMM目的: java内存模型主要定义了各种变量的访问规则。
这里的变量指的是实例字段、静态字段、构成数组对象的元素,不包括局部变量和方法参数,即定义的是共享变量的访问规则!
这里的访问规则指的是主存和线程工作内存的变量值同步的方式。
volatile是为了解决JMM带来的变量可见性问题。
如果共享变量i++也和局部变量i++的执行流程相同:直接将局部变量中i值自增加1,那么volatile不就能保证多线程数据安全了?众所周知,volatile无法保证原子性,它只保证可见性。来看看JVM的具体实现:
public class Demo {
static volatile 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 //将栈顶两int值相加并将结果压入栈顶
5: putstatic #2 // Field 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;
很明显了,原因就是java共享变量的运算操作符不是原子操作!(字节码层面。其实不够严谨,因为就算编译出来只有一条字节码指令,JVM解释器也会运行多行代码解释执行或即时编译器也会编译成多行本地机器码执行,使用 -XX:+PrintAssembly 参数输出反汇编来分析会更严谨些,但这里字节码层面已经足以说明问题)
多线程环境,假设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++线程安全的原因。简单讲就是volatile关键字只保证了 “0: getstatic” 获取到的是主存中最新的值,不保证 “4: iadd” 执行时操作栈中的值是主存最新的。
其实,这些都是JMM Java内存模型带来的数据问题:可见性、有序性、原子性。volatile是JDK提供的解决JMM数据可见性的关键字(volatile还保证了有序性),JVM实现volatile内存可见性语义,上面反汇编得到的代码就是JVM的具体实现流程。