为什么 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 字段而不是临时变量重新加载锁。但是,这只是合法的,因为 lockfinal!)

但是...Java 编译器策略不是优化 javac 生成的字节码。相反,繁重的优化是在 JIT 编译时完成的。在这一点上,人们会期望本机代码将锁保存在寄存器中……如果这是最佳做法的话。


"extra variable" 可能是您正在使用的反编译器的产物。它不理解编译器使用的习惯用法。它添加了一个局部变量,却不知道该变量是在它对您隐藏的合成处理程序块中使用的。

将反编译器所说的内容视为 "truth" 绝非明智之举。众所周知,反编译代码可能具有误导性……甚至不是有效的 Java 代码。

当然,任何仅基于反编译器输出的代码优化观察都是没有价值的。


1 - 实际上,如果考虑 Thread.kill() 和用于实现它的 ThreadDeath 异常,这不是假设。即使是空块。