目次
マルチスレッドでのスタティック変数・メソッドの効果を検証してみた
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かそうではないかは影響しないことが確認できました。