Java で活性エラーが起きたときの x86 アセンブリコードを見てみました。
Effective Java の「項目66 共有された可変データへのアクセスを同期する」で、活性エラー*1について記載があります。
この現象が起こった時の x86 アセンブリコードを見てみました。
なお、アセンブリコードの出力は以下を参考にさせていただきました*2。
JITの出力するx64アセンブリを深追いしてみた - 川口耕介の日記
また、今回使用したのは以下のバージョンです*3。
Java™ Platform, Standard Edition 6u25 Binary Snapshot Releases
活性エラーが起きたとき
まずは、Effective Java に記載されている、活性エラーが起きる場合(変数を同期していないコード)を試してみました。
import java.util.concurrent.TimeUnit; public class StopThread { private static boolean stopRequested; public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(new Runnable() { @Override public void run() { int i = 0; while (!stopRequested) { i++; } } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; } }
これを、「-XX:+PrintOptoAssembly -server」オプションをつけて実行します。
すると、以下のような内容が出力されました。
004 B1: # B3 B2 <- BLOCK HEAD IS JUNK Freq: 1 004 PUSHL EBP SUB ESP,24 # Create frame 00b MOV EBX,#2 010 ADD EBX,[ECX] 012 MOV [ESP + #0],ECX 015 CALL_LEAF,runtime OSR_migration_end No JVM State Info # 01a MOV ECX,#336 01f MOVZX8 EAX,[ECX + precise klass StopThread: 0x041b1e98:Constant:exact *] # ubyte -> int ! Field StopThread.stopRequested 026 TEST EAX,EAX 028 Jne,s B3 P=0.000000 C=123357.000000 028 02a B2: # B2 <- B1 B2 top-of-loop Freq: 1e-035 02a TSTL #polladdr,EAX ! Safepoint: poll for GC # StopThread$1::run @ bci:11 L[0]=_ L[1]=EBX STK[0]=EAX # OopMap{off=42} 030 INC EBX 031 JMP,s B2
たしかに、Effective Java に書いてあるように、stopRequested 変数へのアクセスがループの外に移動してしまっています。
Java のコードで考えると、以下のような感じのようです。
if(!stopRequested) // 0x028 while(true) // 0x031 i++; // 0x030
活性エラーが起きなかったとき
続いて、stopRequested 変数に volatile 宣言を付けて、同期をとるようにした場合の結果を見てみます。
private static volatile boolean stopRequested;
004 B1: # B3 <- BLOCK HEAD IS JUNK Freq: 1 004 PUSHL EBP SUB ESP,24 # Create frame 00b MOV EBP,#1 010 ADD EBP,[ECX] 012 MOV [ESP + #0],ECX 015 CALL_LEAF,runtime OSR_migration_end No JVM State Info # 01a MOV EBX,#336 01f JMP,s B3 NOP # 15 bytes pad for loops and calls 030 B2: # B3 <- B3 top-of-loop Freq: 1e+006 030 INC EBP 031 031 B3: # B2 B4 <- B1 B2 Loop: B3-B2 inner Freq: 1e+006 031 MOVZX8 ECX,[EBX + precise klass StopThread: 0x041b1f68:Constant:exact *] # ubyte -> int ! Field VolatileStopThread.stopRequested 038 MEMBAR-acquire ! (empty encoding) 038 TSTL #polladdr,EAX ! Safepoint: poll for GC # StopThread$1::run @ bci:11 L[0]=_ L[1]=EBP STK[0]=ECX # OopMap{off=56} 03e TEST ECX,ECX 040 Je,s B2 P=1.000000 C=126802.000000
今度は、Java で書いたコードの通りに、stopRequested の読み込みがループ内にあります。
while (!stopRequested) // 0x031 〜 0x040 i++; // 0x030
変数の同期を取っていないのに、ちゃんと動くパターン(活性エラーにならない)
さらに、stopRequested 変数に volatile 宣言を外して、「 i++; 」の代わりに「 this.hashCode(); 」としてみました。
while (!stopRequested) { this.hashCode(); }
すると…。
048 B6: # B8 <- B5 Freq: 0.00199894 # Block is sole successor of call 048 JMP,s B8 048 04a B7: # B16 B8 <- B9 B11 top-of-loop Freq: 1998.98 04a MOV ECX,EBP 04c NOP # 3 bytes pad for loops and calls 04f CALL,static java.lang.Object::hashCode # StopThread$1::run @ bci:6 L[0]=EBP L[1]=_ # OopMap{ebp=Oop off=84} 054 054 B8: # B12 B9 <- B6 B4 B7 B10 Loop: B8-B7 inner Freq: 999998 054 MOV ECX,#336 059 MOVZX8 EBX,[ECX + precise klass StopThread: 0x041be830:Constant:exact *] # ubyte -> int ! Field StopThread.stopRequested 060 TSTL #polladdr,EAX ! Safepoint: poll for GC # StopThread$1::run @ bci:13 L[0]=EBP L[1]=_ STK[0]=EBX # OopMap{ebp=Oop off=96} 066 TEST EBX,EBX 068 Jne,s B12 P=0.000000 C=91248.000000 068 06a B9: # B7 B10 <- B8 Freq: 999998 06a MOV ECX,[EBP] # int 06d MOV EDI,ECX 06f AND EDI,#7 072 CMP EDI,#1 075 Jne,s B7 P=0.001000 C=-1.000000
活性エラーが起きず、1秒後にきちんと終了するようになりました。同期していないのに。
メソッド呼び出しが入ると活性エラーは起きない…、みたいです。
ただ、単純にメソッド呼び出しが入れば活性エラーが起きなくなるのではないようです。
たとえば、メソッド呼び出しをしていても、VM最適化によって呼び出し自体が無くなるような場合*4は、活性エラーが起きました。
言語仕様として、メソッド呼び出しをした後は変数を読み直す、という規定があるわけではないので、処理の都合上そうなっているだけだと思います。
なので、メソッド呼び出しをしているから変数を同期化しないというようにするのではなく、言語仕様に乗っ取って、ちゃんと同期化する方がよさそうです。
ところで
… やっては見たものの、あまりアセンブラに関して詳しくないので、間違えていたらごめんなさい (><)