1. 概要

このチュートリアルでは、シングルトンスコープで作成されたSpring Beanが、複数の同時リクエストを処理するために舞台裏でどのように機能するかを学習します。 さらに、Javaがbeanインスタンスをメモリに格納する方法と、それらへの同時アクセスを処理する方法についても理解します。

2. SpringBeansとJavaヒープメモリ

Javaヒープは、ご存知のとおり、アプリケーション内で実行中のすべてのスレッドがアクセスできるグローバル共有メモリです。 SpringコンテナがシングルトンスコープのBeanを作成すると、そのBeanはヒープに格納されます。このようにして、すべての同時スレッドが同じBeanインスタンスを指すことができます。

次に、スレッドのスタックメモリとは何か、およびスレッドが同時要求を処理するのにどのように役立つかを理解しましょう。

3. 同時リクエストはどのように提供されますか?

例として、ProductServiceと呼ばれるシングルトンbeanを持つSpringアプリケーションを見てみましょう。

@Service
public class ProductService {
    private final static List<Product> productRepository = asList(
      new Product(1, "Product 1", new Stock(100)),
      new Product(2, "Product 2", new Stock(50))
    );

    public Optional<Product> getProductById(int id) {
        Optional<Product> product = productRepository.stream()
          .filter(p -> p.getId() == id)
          .findFirst();
        String productName = product.map(Product::getName)
          .orElse(null);

        System.out.printf("Thread: %s; bean instance: %s; product id: %s has the name: %s%n", currentThread().getName(), this, id, productName);

        return product;
    }
}

このBeanには、呼び出し元に製品データを返すメソッド getProductById()があります。 さらに、このBeanによって返されるデータは、エンドポイント / product /{id}上のクライアントに公開されます。

次に、同時呼び出しがエンドポイント / product /{id}にヒットしたときに実行時に何が起こるかを調べてみましょう。 具体的には、最初のスレッドはエンドポイント / product / 1 を呼び出し、2番目のスレッドは / product /2を呼び出します。

Springは、リクエストごとに異なるスレッドを作成します。 以下のコンソール出力に示されているように、両方のスレッドは同じProductServiceインスタンスを使用して製品データを返します。

Thread: pool-2-thread-1; bean instance: [email protected]; product id: 1 has the name: Product 1
Thread: pool-2-thread-2; bean instance: [email protected]; product id: 2 has the name: Product 2

Springが複数のスレッドで同じbeanインスタンスを使用する可能性があります。これは、最初に、Javaが各スレッドに対してプライベートスタックメモリを作成するためです。

スタックメモリは、スレッドの実行中にメソッド内で使用されるローカル変数の状態を格納する役割を果たします。このように、Javaは、並列で実行されるスレッドが互いの変数を上書きしないようにします。

次に、 ProductService Beanはヒープレベルで制限やロックを設定しないため、各スレッドのプログラムカウンターは、ヒープメモリ内のBeanインスタンスの同じ参照を指すことができます。したがって、両方のスレッドで getProdcutById()メソッドを同時に実行できます。

次に、シングルトンBeanがステートレスであることが重要である理由を理解します。

4. ステートレスシングルトンビーンズvs. ステートフルシングルトンビーンズ

ステートレスシングルトンBeanが重要である理由を理解するために、ステートフルシングルトンBeanを使用することの副作用を見てみましょう。

productName変数をクラスレベルに移動したとします。

@Service
public class ProductService {
    private String productName = null;
    
    // ...

    public Optional getProductById(int id) {
        // ...

        productName = product.map(Product::getName).orElse(null);

       // ...
    }
}

それでは、サービスを再度実行して、出力を見てみましょう。

Thread: pool-2-thread-2; bean instance: [email protected]; product id: 2 has the name: Product 2
Thread: pool-2-thread-1; bean instance: [email protected]; product id: 1 has the name: Product 2

ご覧のとおり、 productId 1を呼び出すと、「Product1」ではなくproductName「Product2」が表示されます。 これは、 ProductService がステートフルであり、実行中のすべてのスレッドと同じproductName変数を共有しているために発生します。

このような望ましくない副作用を回避するには、シングルトンBeanをステートレスに保つことが重要です。

5. 結論

この記事では、SpringでシングルトンBeanへの同時アクセスがどのように機能するかを見ました。 まず、JavaがシングルトンBeanをヒープメモリに格納する方法を確認しました。 次に、異なるスレッドがヒープから同じシングルトンインスタンスにアクセスする方法を学びました。 最後に、ステートレスBeanを使用することが重要である理由について説明し、Beanがステートレスでない場合に発生する可能性のある例を確認しました。

いつものように、これらの例のコードはGitHubから入手できます。