コード型ログ(2) staticな変数の排他にはsynchronized(*.class) { } を使う

他の人が書いていたら読めるけれども、知らなきゃ書けない定型文をあつめたコード型ログを作っている。今回は2回目。

前回: コード型ログ(1) スレッドを止めるにはinterruptを使う - 超ウィザード級ハッカーのたのしみ


いい設計とは言えないかもしれないが、staticな変数の排他を取りたい時がある。この場合には、synchronized(*.class) { }を使用する。Classオブジェクトは基本的にはstaticでユニークな、つまりシングルトンなオブジェクトである。*1

package org.example.katalog;

public class SynchronizedClass {
  static int count = 0;

  public void run() {
    Thread[] ts = new Thread[5];
    for (int i = 0; i < ts.length; i++) {

      // count変数の値をプリントして、カウントアップすることを10回繰り返す。
      Thread t = new Thread() {
        @Override
        public void run() {
          for (int j = 0; j < 10; j++) {

            // SynchronizedClass クラスのロックを取る。
            synchronized (SynchronizedClass.class) {
              System.out.println(count);
              count++;
            }

          }
        }
      };

      t.start();
      ts[i] = t;
    }

    for (Thread t : ts) {
      try {
        t.join();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public static void main(String[] args) {
    new SynchronizedClass().run();
  }
}

悪い例は、ロック用のオブジェクトを作ることである。

public class SynchronizedClassBad {
  // ロックオブジェクト
  private static final Object LOCK_OBJ = new Object();
  static int count = 0;

  public void run() {
    Thread[] ts = new Thread[5];
    for (int i = 0; i < ts.length; i++) {

      // count変数の値をプリントして、カウントアップすることを10回繰り返す。
      Thread t = new Thread() {
        @Override
        public void run() {
          for (int j = 0; j < 10; j++) {

            // SynchronizedClass クラスのロックを取る。
            synchronized (LOCK_OBJ) {
              System.out.println(count);
              count++;
            }
// 以下略

なお、staticメソッドにsynchronizedをつけたもの

synchronized static void method() {
  // 処理
}

static void method() {
  synchronized (this.getClass()) {
    // 処理
  }
}

と等価である。

*1:ClassLoaderでややこしいことをしたら別。

コード型ログ(3) privateなメソッドのテスト

前回: コード型ログ(2) staticな変数の排他にはsynchronized(*.class) { } を使う - 超ウィザード級ハッカーのたのしみ


privateなメソッドは、ユニットテストがしにくい。

対処法は2つで、

  1. テストしないか、
  2. Reflectionで頑張るか、
  3. スコープをpackage pririveteに広げるか、

である。

呼び出しものの入出力からテストができるのであれば、無理してテストをする必要はない。

しかし、わざわざ別のメソッドに分けたということは、入力と出力の対応が明確な単位になっているはずなので、テストしたい。

Reflectionで頑張る場合は、例えば

class Add {
  private static int add(int a, int b) {
    return a + b;
  }
}

を呼ぶには、

  static int invokeAdd(int a, int b) {
    Method m = null;
    try {
      // getMethod では private メソッドは取得できない。
      m = Add.class.getDeclaredMethod(
          "add", int.class, int.class);
    } catch (NoSuchMethodException | SecurityException e) {
      throw new RuntimeException(e);
    }

    // メソッドにアクセスできるようにする。
    // false の場合は、invoke した際に IllegalAccessException となる。
    m.setAccessible(true);

    int r;
    try {
      // インスタンスメソッドの場合は、第一引数はインスタンスとなる。
      r = (int)m.invoke(null, 1, 1);
    } catch (IllegalAccessException | IllegalArgumentException
        | InvocationTargetException e) {
      throw new RuntimeException(e);
    }

    return r;
  }

というような private メソッド呼ぶためのメソッドを作っておいて、そのメソッドの入出力をテストする。このメソッドは読めないので、何をしたいのかはコメントしておくこと。

これが面倒だという場合は、privateにするのは諦めて、アクセス修飾子をdefault (pacakge private)にして、テストから見えるようにする。その際は、テストの時だけ呼び出して良いことを@VisibleForTestingなどで明記しておく。

VisibleForTesting (Guava: Google Core Libraries for Java 19.0 API)

static, final, private なメソッドはメソッド呼び出しのオーバヘッドをなくすためにインライン展開されることが期待できるが、private なインスタンスメソッドを default にしてしまうとこの効果は期待できなくなってしまうので、final をつけるようにするのがよいかもしれない。効果のほどは未検証。privateとfinalが同じ戦略でインライン展開されるかは疑わしい。

privateであっても、テストの時だけは見えるようになってくれれば良いのだけれど。

public ClassnameTest tests Classname {

とか

@TestFor(Classname.class)
public ClassnameTest {

と書いたら、privateメソッドが見えてくれれば便利なのになあ。