LoginSignup
76
79

More than 1 year has passed since last update.

ショロカレ 21日目 SpringのDIはSingleton(シングルトン)

Last updated at Posted at 2016-12-20

はじめに

この記事は初老丸アドベントカレンダー21日目です。

初老になってくるとStruts1系が懐かしく感じてきます。新人の頃よく上司から「Struts1系のActionクラスはSingletonだからスレッドセーフを意識しろ」と言われたものです。
ここでは、SpringのDIもSingletonでスレッドアンセーフだからスレッドセーフを意識してね、ということを簡易なコードで検証したうえで、どのような点に留意しながら実装すれば良いかを簡潔に述べたいと思います。

検証してみる

例えば以下のようなControllerクラスがあるとします(説明の便宜上あまり意味のない実装になっています)。@Controllerを付与したControllerクラスもDI対象なので、このクラスはシングルトンです。そのため、全てのリクエストでこのクラスのインスタンスを共有します。また、このクラスはインスタンス変数のuserIdを持っています。シングルトンなクラスのインスタンス変数は全てのスレッドで共有されます(ローカル変数は、スレッド固有メモリのスタックという領域に格納されるため共有されません)。

HogeController.java
@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";
    }
}
hoge.html
<!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

1.png

この時点でuserIdの値はnullとなっています。

次に以下のユーザBとして以下でアクセスしてみます。
 http://localhost:8080/hoge?id=B

3.png

ユーザBでアクセスしているにもかかわらず、userIdはAとなっています。つまり、インスタンス変数userIdはユーザAでアクセスした状態を保持しています。このことから、SpringのDIもSingletonでありスレッドアンセーフということがわかります。
※ちなみに、this.hashCode()でこのクラスのハッシュコードも確認しましたが常に同じ値を返してきます。

なにがマズイのか

例えば同時に2つのリクエストがあった場合(以下の図のユーザAのリクエストとユーザBのリクエストが同時にあった場合)、以下の現象が起こり得る可能性があります。
4.png

  1. ユーザAのスレッドがuserIdにリクエストパラメータのAを設定する(userId = id)。
  2. スレッドがユーザBのスレッドに切り替わる。
  3. ユーザBのスレッドがuserIdにリクエストパラメータのBを設定する(userId = id)。  
    ※つまりuserIdはBで上書きされる。
  4. スレッドがユーザAのスレッドに切り替わる。
  5. ユーザAのスレッドがuserIdをModelに設定する(model.addAttribute("userId", userId))。  
    ※3でuserIdがBで上書きされたので、この時のuserIdはBとなっている。
  6. ユーザAのスレッドが終了(return "hoge")。
  7. ユーザBのスレッドに切り替わる。
  8. ユーザBのスレッドがuserIdをModelに設定する(model.addAttribute("userId", userId))。  
    ※3でuserIdにBが設定されたので、この時のuserIdはBとなっている。
  9. ユーザ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は以下のようなイメージです。

2.png

以上、初老でした。

76
79
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
79