Atomicity of Reference assignment - Java

     Java's JVM specifications (http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf) claims that writes to and reads of references is atomic on both 32 bit and 64 bit implementation and i happened to have a code which enforced mutual exclusion on the destination variable,

public void set(Test ref)
{
  synchronized (this)
  {

     // obj is the member variable
     obj = ref;
  }

}

   At some point the need for synchronization was in question, as write into a reference is atomic and i started looking at underlying assembly instructions for the above code without synchronized block. I used 64 bit implementation of Open JDK 7 Update 3 on Ubuntu running on Intel x86 64 bit processor. The assembly code turned out as,

Decoding compiled method 0x00007f8c5d061110:
Code:
[Entry Point]
[Constants]
# {method} 'set' '(LTest;)V' in 'Test'
# this: rsi:rsi = 'Test'
# parm0: rdx:rdx = 'Test'
# [sp+0x20] (sp of caller)
0x00007f8c5d061240: mov 0x8(%rsi),%r10d
0x00007f8c5d061244: cmp %r10,%rax
0x00007f8c5d061247: jne 0x00007f8c5d0378a0 ; {runtime_call}
0x00007f8c5d06124d: xchg %ax,%ax
[Verified Entry Point]
0x00007f8c5d061250: push %rbp
0x00007f8c5d061251: sub $0x10,%rsp
0x00007f8c5d061255: nop ;*synchronization entry ; - Test::set@-1 (line 19)
0x00007f8c5d061256: mov %rsi,%r10
0x00007f8c5d061259: mov %rdx,%r8
0x00007f8c5d06125c: mov %r8d,0x10(%rsi)
0x00007f8c5d061260: shr $0x9,%r10
0x00007f8c5d061264: mov $0x7f8c661d7000,%r11
0x00007f8c5d06126e: mov %r12b,(%r11,%r10,1) ;*putfield obj ; - Test::set@2 (line 19)
0x00007f8c5d061272: add $0x10,%rsp
0x00007f8c5d061276: pop %rbp
0x00007f8c5d061277: test %eax,0xcc88d83(%rip) # 0x00007f8c69cea000 ; {poll_return}
0x00007f8c5d06127d: retq 
0x00007f8c5d06127e: hlt 
0x00007f8c5d06127f: hlt 

   It uses two move instructions, one (mov%rdx, %r8) to copy the input reference (param0) into a intermediate register (r8) and the other one (move %r8d, 0x10(%rsi)) to copy the 32bits of register (r8) into the destination reference.

   What happens if the CPU's context changed after the first move instruction and other thread tries to access the value of the reference (obj)? It would end up getting the old value of the reference, despite the fact that an earlier thread had started the copy process of the reference with a new value. Is this desirable and expected? Its up to the application's requirements. In my case, i wanted to ensure a first come first serve (FCFS) behavior for threads. The thread which started the copy of source reference had to complete with a successful write into the destination reference before other threads could use the value of destination reference. Hence, i had to use either a synchronized block or AtomicReference.

    The synchronized block in the Test function translates well into the assembly, and enforces mutual exclusion over the shared reference,

[Entry Point]
[Constants]
# {method} 'set' '(LTest;)V' in 'Test'
# this: rsi:rsi = 'Test'
# parm0: rdx:rdx = 'Test'

# [sp+0x40] (sp of caller)
0x00007f713905f340: mov 0x8(%rsi),%r10d
0x00007f713905f344: cmp %r10,%rax
0x00007f713905f347: jne 0x00007f71390378a0 ; {runtime_call}
0x00007f713905f34d: xchg %ax,%ax
[Verified Entry Point]
0x00007f713905f350: mov %eax,-0x6000(%rsp)
0x00007f713905f357: push %rbp
0x00007f713905f358: sub $0x30,%rsp ;*synchronization entry ; - Test::set@-1 (line 19)
0x00007f713905f35c: mov %rdx,(%rsp)
0x00007f713905f360: mov %rsi,%rbp
0x00007f713905f363: mov (%rsi),%rax
0x00007f713905f366: mov %rax,%r10
0x00007f713905f369: and $0x7,%r10
0x00007f713905f36d: cmp $0x5,%r10
0x00007f713905f371: jne 0x00007f713905f3da
0x00007f713905f373: mov $0xcc29d340,%r11d ; {oop('Test')}
0x00007f713905f379: mov 0xb0(%r11),%r10
0x00007f713905f380: mov %r10,%r11
0x00007f713905f383: or %r15,%r11
0x00007f713905f386: mov %r11,%r8
0x00007f713905f389: xor %rax,%r8
0x00007f713905f38c: test $0xffffffffffffff87,%r8
0x00007f713905f393: jne 0x00007f713905f50e ;*monitorenter ; - Test::set@3 (line 19)

0x00007f713905f399: mov (%rsp),%r10
0x00007f713905f39d: mov %r10,%r11
0x00007f713905f3a0: mov %r11d,0x10(%rbp)

0x00007f713905f3a4: mov %rbp,%r10
0x00007f713905f3a7: shr $0x9,%r10
0x00007f713905f3ab: mov $0x7f7142670000,%r11
0x00007f713905f3b5: mov %r12b,(%r11,%r10,1)
0x00007f713905f3b9: mov $0x7,%r10d
0x00007f713905f3bf: and 0x0(%rbp),%r10
0x00007f713905f3c3: cmp $0x5,%r10
0x00007f713905f3c7: jne 0x00007f713905f445 ;*monitorexit ; - Test::set@10 (line 22) 

     Bottom line, writes to and reads from reference is atomic but assignment (copy from source and write into destination) of a reference isn't atomic and when in doubt, source of truth is in assembly instructions. :-)

  

   

1 comment:

Anonymous said...

This situation is common - it requires either to accept the fact that old value may be provided, or the get() method must also use lock, just as the set() method.