synchronized メソッドの挙動を JVM のスレッドダンプを見ながら確かめる

最近、趣味で Java 製プロダクトをいじっていたり、デザインパターン入門マルチスレッド編を読んでいることもあって Java のコードを書くようになった。
これまでほぼ PHP しかやってこなかったので java.util.concurrent パッケージ の充実っぷりに衝撃をうけた。これらのクラスを使って分散アルゴリズムの実装に挑戦してみたい。

今回はスレッドの排他制御の仕組みである synchronized メソッドの挙動(ブロックされる範囲)を、 JVM のスレッドダンプを見ながら確かめてみる。挙動を確かめるだけならコードを動かすだけで充分なのだが、今後も Java を書くならスレッドダンプを見ることに慣れておいたほうが良いだろうということで。

コード

よくある銀行口座から引き落とすコード。引き落としを行う withdraw() は synchronized メソッドになっているので、ある時点で実行できるスレッドは1つだけ。
引き落とし処理には約10秒かかるので、その間もう片方のスレッドがブロックされているか否かがスレッドダンプから見て取れるはず。

class Bank {
    private int money;

    public Bank(int money) {
        this.money = money;
    }

    public synchronized boolean withdraw(int m) {
        System.out.println("引き落とし中...");

        try {
            Thread.sleep(10 * 1000);
        } catch (InterruptedException e) {
            System.out.println(e);
        }

        if (money < m) {
            return false;
        }

        money -= m;
        System.out.println("引き落とし完了!");
        return true;
    }
}

class Withdraw extends Thread {
    private Bank bank;

    public Withdraw(Bank bank) {
        this.bank = bank;
    }

    @Override
    public void run() {
        if (!bank.withdraw(1000)) {
            System.out.println("口座残高が足りません!");
        }
    }
}

同一インスタンスの synchronozed メソッド呼び出しはブロックされる

コード
public class Sample {
    public static void main(String[] args) {
        Bank bank = new Bank(1000);
        new Withdraw(bank).start();
        new Withdraw(bank).start();
    }
}
スレッドダンプ

スレッドダンプに、スレッドの状態を表す Thread.State が出力されるので、これを確認する。
Thread-1 が BLOCKED なのでブロックされている。

$ jps | grep Sample | cut -d ' ' -f 1 | xargs -I{} jstack {}
...
...
...


"Thread-1" #12 prio=5 os_prio=31 tid=0x00007ff9cb87b000 nid=0x5b03 waiting for monitor entry [0x000070000b3b2000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at Bank.withdraw(Sample.java:33)
        - waiting to lock <0x000000076ac29250> (a Bank)
        at Withdraw.run(Sample.java:19)

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007ff9cb87f000 nid=0x5903 waiting on condition [0x000070000b2af000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at Bank.withdraw(Sample.java:36)
        - locked <0x000000076ac29250> (a Bank)
        at Withdraw.run(Sample.java:19)

...
...
...

異なるインスタンスの synchronozed メソッド呼び出しはブロックされない

コード
public class Sample {
    public static void main(String[] args) {
        Bank bank = new Bank(1000);
        Bank bank2 = new Bank(1000);
        new Withdraw(bank).start();
        new Withdraw(bank2).start();
    }
}
スレッドダンプ

Thread-1, Thread-0 どちらも TIMED_WAITING で、今回でいうところの引き落とし処理中であることがわかる。

$ jps | grep Sample | cut -d ' ' -f 1 | xargs -I{} jstack {}
...
...
...

"Thread-1" #12 prio=5 os_prio=31 tid=0x00007f9e84819000 nid=0x5b03 waiting on condition [0x0000700002cf9000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at Bank.withdraw(Sample.java:36)
        - locked <0x000000076ac29100> (a Bank)
        at Withdraw.run(Sample.java:19)

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007f9e8300d800 nid=0x5903 waiting on condition [0x0000700002bf6000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at Bank.withdraw(Sample.java:36)
        - locked <0x000000076ac290f0> (a Bank)
        at Withdraw.run(Sample.java:19)

...
...
...

クラスメソッドの場合

withdraw() をクラスメソッドにした場合。

コード
public class Sample {
    public static void main(String[] args) {
        new Withdraw().start();
        new Withdraw().start();
    }
}

class Bank {
    private static int money = 1000;

    public static synchronized boolean withdraw(int m) {
        System.out.println("引き落とし中...");

        try {
            Thread.sleep(10 * 1000);
        } catch (InterruptedException e) {
            System.out.println(e);
        }

        if (money < m) {
            return false;
        }

        money -= m;
        System.out.println("引き落とし完了!");
        return true;
    }
}

class Withdraw extends Thread {
    @Override
    public void run() {
        if (!Bank.withdraw(1000)) {
            System.out.println("口座残高が足りません!");
        }
    }
}
スレッドダンプ

もちろんブロックされている。

"Thread-1" #12 prio=5 os_prio=31 tid=0x00007fe10b86a000 nid=0x5b03 waiting for monitor entry [0x000070000412c000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at Bank.withdraw(Sample.java:36)
        - waiting to lock <0x000000076aed9680> (a java.lang.Class for Bank)
        at Withdraw.run(Sample.java:21)

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007fe10b869800 nid=0x5903 waiting on condition [0x0000700004029000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at Bank.withdraw(Sample.java:39)
        - locked <0x000000076aed9680> (a java.lang.Class for Bank)
        at Withdraw.run(Sample.java:21)

異なるインスタンスでクラスメソッドを実行した場合

※ インスタンスでクラスメソッドを実行するのはそもそも良くないコードだが、挙動の確認として。

コード
public class Sample {
    public static void main(String[] args) {
        Bank bank = new Bank();
        Bank bank2 = new Bank();
        new Withdraw(bank).start();
        new Withdraw(bank2).start();
    }
}

/** Bank クラスは同じなので省略 **/

class Withdraw extends Thread {
    private Bank bank;

    public Withdraw(Bank bank) {
        this.bank = bank;
    }

    @Override
    public void run() {
        if (!bank.withdraw(1000)) {
            System.out.println("口座残高が足りません!");
        }
    }
}
スレッドダンプ

「クラスメソッドの場合」と同様にブロックされている。

"Thread-1" #12 prio=5 os_prio=31 tid=0x00007fedf1031800 nid=0x5b03 waiting for monitor entry [0x00007000066d9000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at Bank.withdraw(Sample.java:36)
        - waiting to lock <0x000000076ac28d78> (a java.lang.Class for Bank)
        at Withdraw.run(Sample.java:21)

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007fedf008e800 nid=0x5903 waiting on condition [0x00007000065d6000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at Bank.withdraw(Sample.java:39)
        - locked <0x000000076ac28d78> (a java.lang.Class for Bank)
        at Withdraw.run(Sample.java:21)