Spring の @Controller@Service などのスレッド セーフティの問題を分析する

Spring の @Controller@Service などのスレッド セーフティの問題を分析する

まず、@Controller @Service がスレッドセーフであるかどうかを尋ねます。

回答: デフォルト構成にはありません。なぜ?デフォルトでは@Controller追加されないので@Scope、追加しない場合は@Scopeデフォルト値のsingleton、シングルトンになります。これは、システムがコントローラー コンテナーを 1 回だけ初期化することを意味するため、各リクエストは同じコントローラー コンテナーであり、もちろんスレッドセーフではありません。栗を取ります:

@RestController
public class TestController {
    
    
	private int var = 0;
	@GetMapping(value = "/test_var")
	public String test() {
    
    
		System.out.println("普通变量var:" + (++var));
		return "普通变量var:" + var ;
	}
}

postman で 3 つのリクエストを送信すると、結果は次のようになります。

通常の変数 var: 1
通常の変数 var: 2
通常の変数 var: 3

これは、彼がスレッドセーフではないことを意味します。どうやってするの?次のように、上記の @Scope アノテーションを彼に追加できます。

@RestController
@Scope(value = "prototype") // 加上@Scope注解,他有2个取值:单例-singleton 多实例-prototype
public class TestController {
    
    
	private int var = 0;
	@GetMapping(value = "/test_var")
	public String test() {
    
    
		System.out.println("普通变量var:" + (++var));
		return "普通变量var:" + var ;
	}
}

このように、各リクエストはコントローラー コンテナを個別に作成するため、各リクエストはスレッドセーフになり、3 つのリクエストの結果は次のようになります。

通常の変数 var: 1
通常の変数 var: 1
通常の変数 var: 1

多くの @Scope アノテーションを持つインスタンス プロトタイプは必ずしもスレッドセーフですか?

@RestController
@Scope(value = "prototype") // 加上@Scope注解,他有2个取值:单例-singleton 多实例-prototype
public class TestController {
    
    
	private int var = 0;
	private static int staticVar = 0;
	@GetMapping(value = "/test_var")
	public String test() {
    
    
		System.out.println("普通变量var:" + (++var)+ "---静态变量staticVar:" + (++staticVar));
		return "普通变量var:" + var + "静态变量staticVar:" + staticVar;
	}
}

3 つのリクエストの結果を見てください。

通常の変数 var:1 - 静的変数 staticVar:1
通常の変数 var:1 - 静的変数 staticVar:2
通常の変数 var:1 - 静的変数 staticVar:3

コントローラーは毎回個別に作成されますが、変数自体は静的であるため、 @Scope アノテーションを追加しても、コントローラーの 100% のスレッド セーフが保証されるわけではありません。したがって、スレッド セーフかどうかは、変数の定義方法とコントローラーの構成によって決まります。それでは、ちょっとした実験をしてみましょう。コードは次のとおりです。

@RestController
@Scope(value = "singleton") // prototype singleton
public class TestController {
    
    
	private int var = 0; // 定义一个普通变量
	private static int staticVar = 0; // 定义一个静态变量
	@Value("${test-int}")
	private int testInt; // 从配置文件中读取变量
	ThreadLocal<Integer> tl = new ThreadLocal<>(); // 用ThreadLocal来封装变量
	@Autowired
	private User user; // 注入一个对象来封装变量
	@GetMapping(value = "/test_var")
	public String test() {
    
    
		tl.set(1);
		System.out.println("先取一下user对象中的值:"+user.getAge()+"===再取一下hashCode:"+user.hashCode());
		user.setAge(1);
		System.out.println("普通变量var:" + (++var) + "===静态变量staticVar:" + (++staticVar) + "===配置变量testInt:" + (++testInt)
				+ "===ThreadLocal变量tl:" + tl.get()+"===注入变量user:" + user.getAge());
		return "普通变量var:" + var + ",静态变量staticVar:" + staticVar + ",配置读取变量testInt:" + testInt + ",ThreadLocal变量tl:"
				+ tl.get() + "注入变量user:" + user.getAge();
	}
}


config:Userで定義したController:Bean以外の補足コード

@Configuration
public class MyConfig {
    
    
	@Bean
	public User user(){
    
    
		return new User();
	}
}

一時的に考えられる変数を定義する方法はたくさんありますが、3 つの http リクエストの結果は次のとおりです。

先取一下user对象中的值:0===再取一下hashCode:241165852
普通变量var:1===静态变量staticVar:1===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1
先取一下user对象中的值:1===再取一下hashCode:241165852
普通变量var:2===静态变量staticVar:2===配置变量testInt:2===ThreadLocal变量tl:1===注入变量user:1
先取一下user对象中的值:1===再取一下hashCode:241165852
普通变量var:3===静态变量staticVar:3===配置变量testInt:3===ThreadLocal变量tl:1===注入变量user:1

シングルトン モードでは、コントローラー内の ThreadLocal でカプセル化された変数のみがスレッドセーフであることがわかります。なぜこのように言うのですか?3 つのリクエストの結果では、ThreadLocal 変数の値のみが毎回 0+1=1 から変化し、他は累積されていることがわかります。ユーザー オブジェクトについては、デフォルト値が 0 で、 2 番目の交換は、時刻がすでに 1 であり、重要なのは、彼の hashCode が同じであることです。これは、同じユーザー オブジェクトがリクエストごとに呼び出されることを意味します。
次に、TestController の @Scope アノテーションの属性を multi-instance: に変更し@Scope(value = "prototype")、その他はすべて変更せずに再度リクエストすると、結果は次のようになります。

先取一下user对象中的值:0===再取一下hashCode:853315860
普通变量var:1===静态变量staticVar:1===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1
先取一下user对象中的值:1===再取一下hashCode:853315860
普通变量var:1===静态变量staticVar:2===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1
先取一下user对象中的值:1===再取一下hashCode:853315860
普通变量var:1===静态变量staticVar:3===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

この結果を分析すると、マルチインスタンス モードでは、通常の変数、設定された変数、および ThreadLocal 変数はすべてスレッド セーフである一方、静的変数とユーザー内の変数 (彼のハッシュコードが同じであることを参照) オブジェクトはスレッド セーフではないことがわかります。つまり、TestController は要求されるたびにオブジェクトを初期化しますが、静的変数のコピーは常に 1 つだけ存在し、挿入されたユーザー オブジェクトのコピーも 1 つだけ存在します。静的変数のコピーが 1 つしかないのは当然ですが、ユーザー オブジェクトを毎回新しいものにする方法はありますか? もちろん:

public class MyConfig {
    
    
	@Bean
	@Scope(value = "prototype")
	public User user(){
    
    
		return new User();
	}	
}

同じアノテーションを config 内の注入された Bean に追加し@Scope(value = "prototype")、再度要求するだけです。

先取一下user对象中的值:0===再取一下hashCode:1612967699
普通变量var:1===静态变量staticVar:1===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1
先取一下user对象中的值:0===再取一下hashCode:985418837
普通变量var:1===静态变量staticVar:2===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1
先取一下user对象中的值:0===再取一下hashCode:1958952789
普通变量var:1===静态变量staticVar:3===配置变量testInt:1===ThreadLocal变量tl:1===注入变量user:1

毎回要求されるユーザー オブジェクトの hashCode は同じではなく、各割り当て前のユーザー内の変数値もデフォルト値の 0 であることがわかります。

要約:

@Controller/@Service などのコンテナーでは、デフォルトでスコープ値はシングルトン-シングルトンであり、これもスレッドセーフではありません。

@Controller/@Service などのコンテナーでは静的変数を定義しないようにしてください。シングルトンであってもマルチインスタンス (プロトタイプ) であっても、スレッドセーフではありません。

デフォルトで挿入される Bean オブジェクトも、スコープが設定されていない場合はスレッドアンセーフです。

変数を定義する必要がある場合は、スレッドセーフである ThreadLocal を使用して変数をカプセル化します。



Spring シングルトン モードでの同時実行性の保証

1. Spring Framework コントローラーとサービスはデフォルトで両方ともシングルトンですが、同時実行が非常に多い場合にスレッド セーフを実現するにはどうすればよいですか?

A. スレッドが有効になると、JVM はそのスレッドに Java スタックを割り当て、現在のスレッドの実行状態をフレーム単位で保存します。あるスレッドで実行されているメソッドをカレントメソッド、カレントメソッドが使用するスタックフレームをカレントフレーム、カレントメソッドが属するクラスをカレントクラス、カレントクラスの定数をカレントクラスといいます。これを現在の定数プールと呼びます。スレッドがメソッドを実行すると、現在の定数プールが追跡されます。

B. スレッドが Java メソッドを呼び出すたびに、JVM はスレッドに対応するスタックにフレームをプッシュし、このフレームが自然に現在のフレームになります。メソッドの実行時、このフレームを使用してパラメータ、ローカル変数、中間計算結果などを保存します。

C および Java スタック上のすべてのデータはプライベートです。どのスレッドも別のスレッドのスタック データにアクセスできません。したがって、マルチスレッドの場合、スタック データ アクセスの同期を考慮する必要はありません。

D. 上記のように、@Controller はシングルトン モードです。つまり、オブジェクトには 1 つのインスタンスしかありません。スレッドコピー[スタック]モードによる同時アクセス

2. スレッド コピーとセキュリティの問題 スレッド コピーは、同時アクセスの目的を達成するためにスタックとフレームを通じてスレッドの分離を実現しますが、何か前提条件はありますか?

A. 上で述べたように、ローカル変数と中間演算結果セットのパラメーターはスレッド分離 ==> 安全ですが、メンバー変数はマルチスレッド呼び出しの影響を受けます。

B. コントローラー内のサービスはすべてメンバー変数ですが、影響を受けますか? サービスもシングルトンであり、主にメソッド呼び出しの実装に使用され、フレーム切り替えに入り、中間結果に変換されます。同様に、シングルトンサービスのメンバー変数とローカル変数のスレッド分離は、コントローラーと同じです。 。
PS: Spring は ThreadLocal を使用して一部の Bean (RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder など) の非スレッドセーフ状態を処理し、それらをスレッドセーフ状態にします。
TransactionSynchronizationManager は、dao の SqlSession (つまり getConnection) の起点であるリソース リソースに ThreadLocal ストレージを採用します。

C. 対応するメンバー変数はすべてのスレッドに公開されるため、スレッドの安全性を確保するには、ThreadLocal を使用してそれらを保護することが最善です。



コントローラーやサービスなどはシングルトンですが、なぜ同時リクエストを処理するのでしょうか?

実際のプロジェクトでは、コントローラー、サービス、および Dao レイヤーのクラスはすべてシングルトンであるため、同時シナリオで各リクエスト間のデータのセキュリティをどのように確保するかが、JVM のヒープ コンテンツに焦点を当てます。

  1. 概要
    シングルトン スコープで作成された Spring Bean がバックグラウンドでどのように動作して複数の同時リクエストを処理するかを学びます。また、Java が Bean インスタンスをメモリに保存する方法と、それらへの同時アクセスを処理する方法についても学習します。

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

  3. 同時リクエストを処理する方法
    例として、ProductService というシングルトン Bean を持つ Spring アプリケーションを見てみましょう。

画像

この Bean には、呼び出し元に製品データを返すメソッド getProductById() があります。さらに、この Bean によって返されるデータは、エンドポイント/product/{id}でクライアントに公開されます。
次に、 /product/{id} も呼び出された場合に実行時に何が起こるかを調べます。具体的には、最初のスレッドはエンドポイント /product/1 を呼び出し
、2 番目のスレッドは */product/2* を呼び出します。
Spring はリクエストごとに異なるスレッドを作成します。以下のコンソール出力からわかるように、両方のスレッドが同じ ProductService インスタンスを使用して製品データを返しています。

画像

Spring は複数のスレッドで同じ Bean インスタンスを使用できます。これは、まず Java がスレッドごとにプライベート スタック メモリを作成するためです。スタック メモリは、スレッド実行中にメソッド内で使用されるローカル変数の状態を保存する役割を果たします。このようにして、Java は、並行して実行されるスレッドが互いの変数を上書きしないようにします。
次に、ProductService Bean にはヒープ レベルで制限やロックが設定されていないため、各スレッドのプログラム カウンタはヒープ メモリ内の Bean インスタンスへの同じ参照を指すことができます。したがって、2 つのスレッドが getProdcutById() メソッドを同時に実行できます。

次に、シングルトン Bean がステートレスであることが重要である理由を学びます。
ステートレス クラス: クラスには状態情報がありません。通常、メンバー変数が存在しないか、メンバー変数の値は変更されません。

  1. ステートレス シングルトン Bean とステートフル シングルトン Bean
    ステートレス シングルトン Bean が重要である理由を理解するには、ステートフル シングルトン Bean の使用による副作用を確認してください。
    productName 変数がクラス レベル (つまりメンバー変数) に移動されると仮定します。

画像

ここで、サービスを再度実行して出力を確認します。

画像

画像

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

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

おすすめ

転載: blog.csdn.net/qq_43842093/article/details/132641959