为什么 Java 编译器在 "synchronized block" 之前添加 "Redundant read"?
Why Java compiler add "Redundant read" before "synchronized block"?
// the java source code
public class Demo {
private final Object lock = new Object();
public void read() {
synchronized (lock) {
// more code here ...
}
}
}
// the decompiled .class file
public class Demo {
private final Object lock = new Object();
public void read() {
// Why Java compiler add this line? Is the 'read this.lock' redundant?
Object var1 = this.lock;
synchronized(this.lock) {
// more code here ...
}
}
}
这里的字节码:javap -l -p -s demo.class
public void read();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
LineNumberTable:
line 15: 0
line 16: 7
line 17: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this Lxechoz/vipshop/com/demo/thread/Demo;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class xechoz/vipshop/com/demo/thread/Demo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
我认为行 1: getfield #3 // Field lock:Ljava/lang/Object;
对应Object var1 = this.lock;
.
我知道编译器会通过添加或删除一些代码来优化代码。
但是,为什么编译器会在同步块之前添加一个read语句。
为什么需要这个?或者为什么它是优化?
来自JLS 3.14
Synchronization in the Java Virtual Machine is implemented by monitor entry and exit, either explicitly (by use of the monitorenter and monitorexit instructions) or implicitly (by the method invocation and return instructions).
为了确保 monitorexit
始终被执行,编译器为 Throwable
添加了一个隐式的 catch
子句。
To enforce proper pairing of monitorenter and monitorexit instructions on abrupt method invocation completion, the compiler generates exception handlers (§2.10) that will match any exception and whose associated code executes the necessary monitorexit instructions.
使用 javap -c Demo
时,您可以在偏移量 12-16
处看到此附加字节码
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
生成的代码为伪代码
Object var1 = this.lock;
try {
monitorenter(var1);
// more code here ...
monitorexit(var1);
} catch (Throwable t) {
monitorexit(var1);
throw t;
}
这是实际的字节码。
public void read();
Code:
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
你会看到有两个地方aload_1
用于从堆栈帧加载锁。
- 第一次是作为偏移量8处的monitorexit的操作数,也就是代码正常退出synchronized块的情况。
- 第二次是当它被用作偏移量 14 处的 monitorexit 的操作数时。这是代码展开假设的 1 异常的情况,其中在重新抛出当前异常之前必须释放监视器锁。
(另见@SubOptimal 的 pseudo-code。)
字节码可以收紧。 (例如,优化的字节码编译器可能意识到它可以从 lock
字段而不是临时变量重新加载锁。但是,这只是合法的,因为 lock
是 final
!)
但是...Java 编译器策略不是优化 javac
生成的字节码。相反,繁重的优化是在 JIT 编译时完成的。在这一点上,人们会期望本机代码将锁保存在寄存器中……如果这是最佳做法的话。
"extra variable" 可能是您正在使用的反编译器的产物。它不理解编译器使用的习惯用法。它添加了一个局部变量,却不知道该变量是在它对您隐藏的合成处理程序块中使用的。
将反编译器所说的内容视为 "truth" 绝非明智之举。众所周知,反编译代码可能具有误导性……甚至不是有效的 Java 代码。
当然,任何仅基于反编译器输出的代码优化观察都是没有价值的。
1 - 实际上,如果考虑 Thread.kill()
和用于实现它的 ThreadDeath
异常,这不是假设。即使是空块。
// the java source code
public class Demo {
private final Object lock = new Object();
public void read() {
synchronized (lock) {
// more code here ...
}
}
}
// the decompiled .class file
public class Demo {
private final Object lock = new Object();
public void read() {
// Why Java compiler add this line? Is the 'read this.lock' redundant?
Object var1 = this.lock;
synchronized(this.lock) {
// more code here ...
}
}
}
这里的字节码:javap -l -p -s demo.class
public void read();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
LineNumberTable:
line 15: 0
line 16: 7
line 17: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this Lxechoz/vipshop/com/demo/thread/Demo;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class xechoz/vipshop/com/demo/thread/Demo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
我认为行 1: getfield #3 // Field lock:Ljava/lang/Object;
对应Object var1 = this.lock;
.
我知道编译器会通过添加或删除一些代码来优化代码。
但是,为什么编译器会在同步块之前添加一个read语句。
为什么需要这个?或者为什么它是优化?
来自JLS 3.14
Synchronization in the Java Virtual Machine is implemented by monitor entry and exit, either explicitly (by use of the monitorenter and monitorexit instructions) or implicitly (by the method invocation and return instructions).
为了确保 monitorexit
始终被执行,编译器为 Throwable
添加了一个隐式的 catch
子句。
To enforce proper pairing of monitorenter and monitorexit instructions on abrupt method invocation completion, the compiler generates exception handlers (§2.10) that will match any exception and whose associated code executes the necessary monitorexit instructions.
使用 javap -c Demo
时,您可以在偏移量 12-16
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
生成的代码为伪代码
Object var1 = this.lock;
try {
monitorenter(var1);
// more code here ...
monitorexit(var1);
} catch (Throwable t) {
monitorexit(var1);
throw t;
}
这是实际的字节码。
public void read();
Code:
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
你会看到有两个地方aload_1
用于从堆栈帧加载锁。
- 第一次是作为偏移量8处的monitorexit的操作数,也就是代码正常退出synchronized块的情况。
- 第二次是当它被用作偏移量 14 处的 monitorexit 的操作数时。这是代码展开假设的 1 异常的情况,其中在重新抛出当前异常之前必须释放监视器锁。
(另见@SubOptimal 的 pseudo-code。)
字节码可以收紧。 (例如,优化的字节码编译器可能意识到它可以从 lock
字段而不是临时变量重新加载锁。但是,这只是合法的,因为 lock
是 final
!)
但是...Java 编译器策略不是优化 javac
生成的字节码。相反,繁重的优化是在 JIT 编译时完成的。在这一点上,人们会期望本机代码将锁保存在寄存器中……如果这是最佳做法的话。
"extra variable" 可能是您正在使用的反编译器的产物。它不理解编译器使用的习惯用法。它添加了一个局部变量,却不知道该变量是在它对您隐藏的合成处理程序块中使用的。
将反编译器所说的内容视为 "truth" 绝非明智之举。众所周知,反编译代码可能具有误导性……甚至不是有效的 Java 代码。
当然,任何仅基于反编译器输出的代码优化观察都是没有价值的。
1 - 实际上,如果考虑 Thread.kill()
和用于实现它的 ThreadDeath
异常,这不是假设。即使是空块。