1. 解决了什么问题

i++ 和 ++i 都是进行的累加操作,基本操作都是加 1 , 如下:

1
2
3
4
5
6
7
int i = 0;
i++;
Assert.that(i == 1, "相等");

int j = 0;
++j;
Assert.that(j == 1, "相等");

2. 误区

i++ 的误区, 累加在赋值后执行

1
2
3
4
5
6
7
8
9
10
11
12
public static void testPlus() {
int i = 0;
int j = i++;
Assert.that(j == 1, "失败,此时 j 等于0");
Assert.that(j == 0, "成功");
}

public static void testPlus1() {
int i = 0;
int j = ++i;
Assert.that(j == 1, "成功");
}

for 循环里面进行累加操作,第一个循环里面为什么 count 为 0 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static void countPlusPlus() {
int count = 0;
for (int i = 0; i < 100; i++) {
count = count++;
}
Assert.that(count == 0, "成功");
Assert.that(count == 100, "失败,此时的值为0");
}

private static void countPlusPlus1() {
int count = 0;
int temp = 0;
for (int i = 0; i < 100; i++) {
temp = count++;
}
Assert.that(count == 100, "成功");
Assert.that(temp == 99, "成功");
}

3. 基本原理

通过 javac 命令,编译源码 testPlus 与 testPlus1 生成 .class文件 字节码,再用 javap -c (javap -v *.class > output.txt) 执行 字节码,查看 jvm 执行指令 结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Classfile /Users/jieming/Documents/studyspaces/java-frame/src/main/java/com/cjm/newcoder/TestPlus.class
Last modified 2019-3-20; size 338 bytes
MD5 checksum e1c48ea904613600b30fe105967f7f04
Compiled from "TestPlus.java"
public class com.cjm.newcoder.TestPlus
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/cjm/newcoder/TestPlus
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 testPlus
#9 = Utf8 testPlus1
#10 = Utf8 SourceFile
#11 = Utf8 TestPlus.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 com/cjm/newcoder/TestPlus
#14 = Utf8 java/lang/Object
{
public com.cjm.newcoder.TestPlus();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 11: 0

// 执行 i++
public void testPlus();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
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)
6: istore_2 // 将数据栈顶的值(0) 取出来赋值给2号存储单元(即变量j,此时i=1,j=0)
7: return // 返回时: i=1,j=0
LineNumberTable:
line 14: 0
line 15: 2
line 16: 7
// 执行 ++i
public void testPlus1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0 // 生成整数0
1: istore_1 // 将整数0 赋值给 1号存储单元(既变量i)
2: iinc 1, 1 // 1号存储单元的值+1 (此时 i=1)
5: iload_1 // 将1号存储单元的值加载到数据栈(此时 i=1,栈顶值为1)
6: istore_2 // 将数据栈顶的值(1) 取出来赋值给2号存储单元(既变量j, 此时i=1,j=1)
7: return // 返回时:i=1, j=1
LineNumberTable:
line 19: 0
line 20: 2
line 21: 7
}
SourceFile: "TestPlus.java"

在执行 i++ 的时候,先把 i 的数据暂存在数据栈,执行 累加后,再把数据栈的数据赋值给相应的变量。

而执行 ++i 的时候,先执行累加,再把数据加载到数据栈,再把数据栈的值赋值相应变量。

4. 多线程并发引发的混乱

多线程环境下 ++i 操作引起的数据混乱。引发混乱的原因是:++i 操作不是原子操作。

从最底层的CPU层面上来说,++i 操作大致可以分解为以下 3个指令:

  1. 取数
  2. 累加
  3. 存储

其中的一条指令可以保证是原子操作,但是 3 条指令合在一起却不是,这就导致了++i语句不是原子操作。

如果变量 ivolatile 修饰是否可以保证++i是原子操作呢,实际上这也是不行的。volatile 并不能保证原子性,它是增加了线程间的可见性。

如果要保证累加操作的原子性,可以采取下面的方法

  1. 将 ++i 置于同步块中,可以是 synchronized 或者J.U.C中的排他锁 (如 ReentrantLock 等)
  2. 使用原子性(Atomic)类替换 ++i,具体使用哪个类由变量类型决定。如果i是整形,则使用AtomicInteger类,其中的AtomicInteger#addAndGet()就对应着 ++i语句,不过它是原子性操作。
4.1 volatile 与 synchronized 的区别

volatile 本质是在告诉 jvm 当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile 仅能使用在变量级别,synchronized 则可以使用在变量,方法。

volatile 仅能实现变量的修改可见性,但不具备原子特性,而 synchronized 则可以保证变量的修改可见性和原子性。

volatile 不会造成线程阻塞,而 synchronized 可能会造成线程的阻塞。

5. 参考

深入理解Java中的i++、++i语句