はじめに
この記事は初老丸アドベントカレンダー21日目です。
初老になってくるとStruts1系が懐かしく感じてきます。新人の頃よく上司から「Struts1系のActionクラスはSingletonだからスレッドセーフを意識しろ」と言われたものです。
ここでは、SpringのDIもSingletonでスレッドアンセーフだからスレッドセーフを意識してね、ということを簡易なコードで検証したうえで、どのような点に留意しながら実装すれば良いかを簡潔に述べたいと思います。
検証してみる
例えば以下のようなControllerクラスがあるとします(説明の便宜上あまり意味のない実装になっています)。@Controllerを付与したControllerクラスもDI対象なので、このクラスはシングルトンです。そのため、全てのリクエストでこのクラスのインスタンスを共有します。また、このクラスはインスタンス変数のuserIdを持っています。シングルトンなクラスのインスタンス変数は全てのスレッドで共有されます(ローカル変数は、スレッド固有メモリのスタックという領域に格納されるため共有されません)。
@Controller
@RequestMapping("/hoge")
public class HogeController {
private String userId;
@GetMapping
public String hoge(Model model, @RequestParam("id") String id) {
System.out.println(this.hashCode());
userId = id;
model.addAttribute("userId", userId);
return "hoge";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="${userId}"></p>
</body>
</html>
試しにこのクラスにブレークポイントを付けてデバッグ実行しながらuserIdを確認してみます。
まずは、ユーザAとして以下でアクセスしてみます。
http://localhost:8080/hoge?id=A
この時点でuserIdの値はnullとなっています。
次に以下のユーザBとして以下でアクセスしてみます。
http://localhost:8080/hoge?id=B
ユーザBでアクセスしているにもかかわらず、userIdはAとなっています。つまり、インスタンス変数userIdはユーザAでアクセスした状態を保持しています。このことから、SpringのDIもSingletonでありスレッドアンセーフということがわかります。
※ちなみに、this.hashCode()でこのクラスのハッシュコードも確認しましたが常に同じ値を返してきます。
なにがマズイのか
例えば同時に2つのリクエストがあった場合(以下の図のユーザAのリクエストとユーザBのリクエストが同時にあった場合)、以下の現象が起こり得る可能性があります。
- ユーザAのスレッドがuserIdにリクエストパラメータのAを設定する(userId = id)。
- スレッドがユーザBのスレッドに切り替わる。
- ユーザBのスレッドがuserIdにリクエストパラメータのBを設定する(userId = id)。
※つまりuserIdはBで上書きされる。 - スレッドがユーザAのスレッドに切り替わる。
- ユーザAのスレッドがuserIdをModelに設定する(model.addAttribute("userId", userId))。
※3でuserIdがBで上書きされたので、この時のuserIdはBとなっている。 - ユーザAのスレッドが終了(return "hoge")。
- ユーザBのスレッドに切り替わる。
- ユーザBのスレッドがuserIdをModelに設定する(model.addAttribute("userId", userId))。
※3でuserIdにBが設定されたので、この時のuserIdはBとなっている。 - ユーザBのスレッドが終了(return "hoge")。
長々と書きましたが、1のユーザAのリクエストでuserIdにAを設定していますが、3でBに上書きされることでユーザBの情報になってしまいます。つまり、ユーザAの情報を表示すべきところをユーザBの情報が表示されてしまうことになります。怖いですね。。この事象は一般的にスレッドアンセーフと呼ばれており好ましくありません。そのため、スレッドセーフにしてやる必要があります。
どう対応するか
主に以下の2通りあります。
1. 状態を持たせないクラス設計にする
Singletonは決して悪ではありません。
- Singletonはインスタンスが1つのためメモリ使用率が低い
- Singletonは最初にインスタンスを生成したら、以降は使い回されるためインスタンス生成コストが2回目利用時以降かからない
といった点から優れています。なので、まずは状態を持たせないようなクラス設計を検討します(今回の例の場合だと、userIdのようなインスタンス変数を持たせないにはどうすべきかを検討します)。
2. DIのスコープ(ライフサイクル)を変更する
どうしても「1. 状態を持たせないクラス設計にする」で対応できない場合は、この方法で対応します。具体的には、@Scopeアノテーションを利用して、インスタンスのスコープ(ライフサイクル)を変更します。指定できるスコープは以下です。
スコープ | 説明 |
---|---|
singleton | コンテナに対してインスタンスを1つだけ作成 |
prototype | 呼び出される毎にインスタンスを作成 |
request | 1回のHTTPリクエスト毎にインスタンスを作成 |
session | HTTPセッション毎にインスタンスを作成 |
今回の例の場合、@Scope("prototype")もしくは@Scope("request")としておけば問題ないでしょう。ただし、スコープ(ライフサイクル)を変更すると、Singletonよりも実行時のパフォーマンスが落ちると言われています。その点は留意しましょう。
SAStrutsのイメージ
ちなみにですが、SAStrutsは以下のようなイメージです。
以上、初老でした。