SpringMVCを使ったJavaプログラムにおけるシングルトンとスタティック変数・メソッドの効果を検証してみた

マルチスレッドでのスタティック変数・メソッドの効果を検証してみた

JavaのSpringMVCを使用して、シングルトンパターンとスタティック変数、スタティックメソッドを実装してみました。これらはJavaアプリケーションでよく使用される概念なんですが、ちょっと混乱してきたので、自分なりの整理結果になります。詳細は以下をご覧ください。

検証ソース

  • コントローラソース
@Controller
@RequestMapping("/hello")
public class HelloWorldController {
    private int counter = 0;

    @RequestMapping("/thread")
    public ModelAndView thread(@RequestParam("id") String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        System.out.println("Parameter:" + id);

        counter = CommonUtil.countup(counter, id);

        CommonUtil com = new CommonUtil();
        counter = com.countdown(counter, id);

        String NewTime1 = CommonUtil.newtime(id);
        String NewTime2 = CommonUtil.newtime(id);

        System.out.println("NewTime1:" + NewTime1);
        System.out.println("NewTime2:" + NewTime2);
        ModelAndView modelAndView = new ModelAndView("thread");
        modelAndView.addObject("id", id);
        modelAndView.addObject("NewTime1", NewTime1);
        modelAndView.addObject("NewTime2", NewTime2);
        return modelAndView;
    }
}
  • 共通処理クラス
public class CommonUtil {

    public static int countup(int var, String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        var++;
        System.out.println(var);
        return(var);
    }

    public int countdown(int var, String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        var--;
        System.out.println(var);
        return(var);
    }

    public static String newtime(String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        // 現在日時を取得
        LocalDateTime now = LocalDateTime.now();
        // フォーマットを指定して文字列に変換
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        String nowStr = now.format(formatter);      
        System.out.println(nowStr);
        return(nowStr);
    }
}
  • 画面表示用HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Thread</title>
</head>
<body>
  <h2>title</h2>
  <p th:text="${id}"></p>
  <p th:text="${NewTime1}"></p>
  <p th:text="${NewTime2}"></p>
</body>
</html>

ソースコードは単純だと思います。
キーはコントローラクラスの変数「private int counter = 0;」
これは SpringのDIコンテナで管理されている Controller のデフォルトのスコープだとシングルトンのためスレッドセーフではありません
では、それを検証していきましょう。

アクセスURL

/hello/thread?id=001
/hello/thread?id=002

マルチスレッドでの検証となるため、URLは二つ用意します。
ログが分かりやすいようにパラメータに id を付与します。
id 自体には処理結果に全く関係ありません。
では、結果です。

結果

順次処理の実行結果

まずは別々にアクセスが来た場合
カウントアップされ直後にカウントダウンされ 1 -> 0、1 -> 0 を繰り返す結果となります。

Method: thread
Parameter:001
Method: countup
1
Method: countdown
0
Method: newtime
2023/04/30 10:04:50
Method: newtime
2023/04/30 10:04:50
NewTime1:2023/04/30 10:04:50
NewTime2:2023/04/30 10:04:50
Method: thread
Parameter:002
Method: countup
1
Method: countdown
0
Method: newtime
2023/04/30 10:04:52
Method: newtime
2023/04/30 10:04:52
NewTime1:2023/04/30 10:04:52
NewTime2:2023/04/30 10:04:52

同時実行の実行結果

では、次に同時実行のケースです。
同時にリクエストが来た場合、以下の結果をなることがあります。
一時的に「counter = 2」になっていることが分かると思います。
要件として「counter」に何をカウントしたいかによりますが、このケースは予期せず起こったケースとなることが多いのではないでしょうか。
もし別々のリクエストでも同じカウントでトータルをカウントしたいケースを想定した場合、「private static int counter = 0;」と明示的にstatic変数にすべきと思います。

Method: thread
Parameter:001
Method: countup
1
Method: thread
Parameter:002
Method: countup
2
Method: countdown
1
Method: newtime
2023/04/30 10:14:05
Method: newtime
2023/04/30 10:14:05
NewTime1:2023/04/30 10:14:05
NewTime2:2023/04/30 10:14:05
Method: countdown
0
Method: newtime
2023/04/30 10:14:13
Method: newtime
2023/04/30 10:14:13
NewTime1:2023/04/30 10:14:13
NewTime2:2023/04/30 10:14:13

ちなみにこの動作確認のやり方としては、1つ目のリクエスト処理中に2つ目のリクエスト処理を先に処理させています。
具体的にはEclipseのデバック機能を使って以下のブレイクポイントを設定しておき、1つ目のリクエスト処理を止めておき、最初に2つ目のリクエスト処理を進めています。

今回は、変数「private int counter = 0;」がキーであるため、
CommonUtilのメソッド「public static int countup(int var, String id)」と「public int countdown(int var, String id)」は
敢えて、countupメソッドの方をstaticメソッドとしていますが、
いずれのケースでも変数「private int counter = 0;」をスタティック変数として使用している結果がわかると思います。
(もし、countdownメソッドの方がスタティック変数を使っていないければ、結果が0にならないはずなので)

補足結果

「public static String newtime(String id)」メソッドでは、変数「private int counter = 0;」を使用していません。
変わりに内部で作成した「String nowStr」を使用しています。
この変数はstaticメソッドのnewtimeメソッド内で作成されていますが、スレッドセーフな変数になります。

public static String newtime(String id) {
        System.out.println("Method: " + new Object(){}.getClass().getEnclosingMethod().getName());
        // 現在日時を取得
        LocalDateTime now = LocalDateTime.now();
        // フォーマットを指定して文字列に変換
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        String nowStr = now.format(formatter);      
        System.out.println(nowStr);
        return(nowStr);
    }

同時実行の実行結果

今回も同時実行して1つ目のリクエスト処理中に2つ目のリクエスト処理を先に処理させています。
ただし、今回は時間がそれぞれ正しく(上書きされずに)取得されて出力されていることがわかると思います。

Method: thread
Parameter:001
Method: countup
1
Method: countdown
0
Method: newtime
Method: thread
Parameter:002
Method: countup
1
Method: countdown
0
Method: newtime
2023/04/30 10:46:22
Method: newtime
2023/04/30 10:46:42
NewTime1:2023/04/30 10:46:22
NewTime2:2023/04/30 10:46:42
2023/04/30 10:46:03
Method: newtime
2023/04/30 10:47:12
NewTime1:2023/04/30 10:46:03
NewTime2:2023/04/30 10:47:12

今回のブレイクポイントは以下に設定しています。

ちなみに画面の表示結果は以下のようになります。

まとめ

最初に書いた通り、キーは変数「private int counter = 0;」です。
これはぱっと見ると、メンバ変数であり、スレッドセーフのように見えますが、SpringのDIコンテナで管理されているインスタントのデフォルトのスコープだと
シングルトン
のためスレッドセーフではありません。
これを常に意識して使いましょう。
補足としてメソッドがstaticかそうではないかは影響しないことが確認できました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です