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は、活性エラーが起きました。


言語仕様として、メソッド呼び出しをした後は変数を読み直す、という規定があるわけではないので、処理の都合上そうなっているだけだと思います。
なので、メソッド呼び出しをしているから変数を同期化しないというようにするのではなく、言語仕様に乗っ取って、ちゃんと同期化する方がよさそうです。

ところで

… やっては見たものの、あまりアセンブラに関して詳しくないので、間違えていたらごめんなさい (><)

*1:変数の同期を指定していないのが原因で、VMが変数へのアクセスをループ外に移動して(巻き上げて)しまい、処理が無限ループしてしまうこと。

*2:この記事がなければ、自分はここまで深追いできませんでした。ありがとうございます!

*3:fastdebug オプションでビルドされた Java は、これしか見つかりませんでした。最新版で試すには自分でビルドするしかなさそうです…。

*4:たとえば、何もしないメソッドを呼び出す、など。