Search code examples
javabytecodesynchronized

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 ...
    }
    }
}

The bytecode 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

I think the line 1: getfield #3 // Field lock:Ljava/lang/Object;

corresponds to Object var1 = this.lock;.

I know the compiler will optimize the code by adding or removing some code.

But, why the compiler add a read statement before the synchronized block.

Why this is needed? Or why it is an optimization?


Solution

  • Here are the actual bytecodes.

      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
    

    You will see that there are two places where there aload_1 is used to load the lock from the stack frame.

    • The first time is when it is to be used as the operand for the monitorexit at offset 8. That is the case where the code exits the synchronized block normally.
    • The second time is when it is to be used as the operand for the monitorexit at offset 14. That is the case where the code is unwinding a hypothetical1 exception, where the monitor lock must be released before rethrowing the current exception.

    (Also see @SubOptimal's pseudo-code.)

    The bytecodes could be tightened up. (For example, an optimizing bytecode compiler might realize that it could reload the lock from the lock field rather than a temporary variable. However, that is only legitimate because lock is final!)

    However ... the Java compiler strategy is NOT to optimize the bytecodes produced by javac. Instead, the heavy duty optimization is done at JIT compilation time. At that point, one would expect the native code to keep the lock in a register ... if that was the optimal thing to do.


    The "extra variable" is probably an artifact of the decompiler that you are using. It doesn't understand the idiom that the compiler uses. It adds a local variable, without understanding that the variable is used in the synthetic handler block that it is hiding from you.

    It is never wise to treat what a decompiler says as "truth". It is well known that decompiled code can be misleading ... or not even valid Java code.

    Certainly any observations on code optimization that are based solely on decompiler output are without merit.


    1 - Actually, if consider Thread.kill() and the ThreadDeath exception that is used to implement it, it is not hypothetical. Even for an empty block.