As part of my personal code kata, I’ve been studying the code of the ArrayBlockingQueue, originally written by Doug Lea. It is a great example of writing concurrent code. For example, the method for taking something out of the queue, while waiting until there actually is something in the queue, looks like this:
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { try { while (count == 0) notEmpty.await(); } catch (InterruptedException ie) { notEmpty.signal(); // propagate to non-interrupted thread throw ie; } E x = extract(); return x; } finally { lock.unlock(); } }
This is seen as the prototypical example of how to use the Java Lock class, so it is interesting enough from that perspective. Many people have written about that bit, so I will not attempt to do a better job. One thing I was unable to figure out though is the first line of take(), in which the field lock is assigned to the local variable (also called lock). Intuitively, it would seem like this line would be necessary if the field is volatile and the method needs a reliable copy. However, the lock field in ArrayBlockingQueue is final, so volatility is not a problem. I found an entry on Stack Overflow which gave a teaser, saying the byte code is more compact and efficient. I did not find this answer sufficient, so I did my own little study to dig deeper into why this makes the byte code more compact.
I copied the methods take and put and any needed supporting fields and method, and decompiled the class to bytecode (using javap -c). In the first compilation, I left the code as is, but in the second I removed the assignment to a local variable:
Original:
public java.lang.Object take() throws java.lang.InterruptedException; Code: 0: aload_0 1: getfield #34; //Field lock:Ljava/util/concurrent/locks/ReentrantLock; 4: astore_1 5: aload_1 6: invokevirtual #78; //Method java/util/concurrent/locks/ReentrantLock.lockInterruptibly:()V 9: goto 21 12: aload_0 13: getfield #40; //Field notEmpty:Ljava/util/concurrent/locks/Condition; 16: invokeinterface #81, 1; //InterfaceMethod java/util/concurrent/locks/Condition.await:()V 21: aload_0 22: getfield #64; //Field count:I 25: ifeq 12 28: goto 43 31: astore_2 32: aload_0 33: getfield #40; //Field notEmpty:Ljava/util/concurrent/locks/Condition; 36: invokeinterface #66, 1; //InterfaceMethod java/util/concurrent/locks/Condition.signal:()V 41: aload_2 42: athrow 43: aload_0 44: invokespecial #84; //Method extract:()Ljava/lang/Object; 47: astore_2 48: aload_2 49: astore 4 51: aload_1 52: invokevirtual #86; //Method java/util/concurrent/locks/ReentrantLock.unlock:()V 55: aload 4 57: areturn 58: astore_3 59: aload_1 60: invokevirtual #86; //Method java/util/concurrent/locks/ReentrantLock.unlock:()V 63: aload_3 64: athrow Exception table: from to target type 9 28 31 Class java/lang/InterruptedException 9 51 58 any
Altered:
public java.lang.Object take() throws java.lang.InterruptedException; Code: 0: aload_0 1: getfield #34; //Field lock:Ljava/util/concurrent/locks/ReentrantLock; 4: invokevirtual #78; //Method java/util/concurrent/locks/ReentrantLock.lockInterruptibly:()V 7: goto 19 10: aload_0 11: getfield #40; //Field notEmpty:Ljava/util/concurrent/locks/Condition; 14: invokeinterface #81, 1; //InterfaceMethod java/util/concurrent/locks/Condition.await:()V 19: aload_0 20: getfield #64; //Field count:I 23: ifeq 10 26: goto 41 29: astore_1 30: aload_0 31: getfield #40; //Field notEmpty:Ljava/util/concurrent/locks/Condition; 34: invokeinterface #66, 1; //InterfaceMethod java/util/concurrent/locks/Condition.signal:()V 39: aload_1 40: athrow 41: aload_0 42: invokespecial #84; //Method extract:()Ljava/lang/Object; 45: astore_1 46: aload_1 47: astore_3 48: aload_0 49: getfield #34; //Field lock:Ljava/util/concurrent/locks/ReentrantLock; 52: invokevirtual #86; //Method java/util/concurrent/locks/ReentrantLock.unlock:()V 55: aload_3 56: areturn 57: astore_2 58: aload_0 59: getfield #34; //Field lock:Ljava/util/concurrent/locks/ReentrantLock; 62: invokevirtual #86; //Method java/util/concurrent/locks/ReentrantLock.unlock:()V 65: aload_2 66: athrow Exception table: from to target type 7 26 29 Class java/lang/InterruptedException 7 48 57 any
You will notice that in the first snippet, the comment “Field lock:Ljava/util/concurrent/locks/Reentrant” appears once, but the same comment, indicating a field access, appears twice in the second snippet. The reason the byte code is longer in the altered version is because every time a field is accessed, the owning object must be pushed on the JVM’s operand stack, and then the getfield opcode pops the object and pushes the field onto the stack (the overall process takes two opcodes). However, when a local variable is accessed, it is put on the stack with a single opcode, which makes the byte code smaller and faster. This is an optimization that should rarely be used, however.
It is not only that the bytecode is smaller, it also saves one load which could lead to a cache miss.