JavaRush /Java Blog /Random-JA /Java の同時実行性。チュートリアル - スレッドセーフな構造。
0xFF
レベル 9
Донецк

Java の同時実行性。チュートリアル - スレッドセーフな構造。

Random-JA グループに公開済み
並列プログラムを実行する際の主なリスク (アトミック性可視性など) を検討した後、上記の落とし穴を防ぐのに役立つクラス設計をいくつか見ていきます。これらの構造の一部はスレッドセーフなオブジェクトを作成し、スレッド間でオブジェクトを安全に共有できるようにします。例として、不変オブジェクトとステートレスオブジェクトを見ていきます。他のビューでは、別のスレッドがスレッド ローカル変数などのデータを変更するのを防ぎます。 すべてのソース コードはGithub で参照できます。 1. 不変オブジェクト 不変オブジェクトには状態 (オブジェクトの状態を表すデータ) がありますが、オブジェクトのインスタンスが作成されると、状態はコンストラクターの作成時に設定され、状態を変更することはできません。スレッドは交互に実行できますが、オブジェクトには依然として 1 つの可能な状態があります。すべてのフィールドは読み取り専用であるため、スレッドはオブジェクトのデータを変更できません。このため、不変ストリームは本質的にスレッドセーフです。Product クラスは不変クラスを示します。コンストラクター内のすべてのフィールドに値が入力され、フィールドは何も変更されません。 public final class Product { private final String id; private final String name; private final double price; public Product(String id, String name, double price) { this.id = id; this.name = name; this.price = price; } public String getId() { return this.id; } public String getName() { return this.name; } public double getPrice() { return this.price; } public String toString() { return new StringBuilder(this.id).append("-").append(this.name) .append(" (").append(this.price).append(")").toString(); } public boolean equals(Object x) { if (this == x) return true; if (x == null) return false; if (this.getClass() != x.getClass()) return false; Product that = (Product) x; if (!this.id.equals(that.id)) return false; if (!this.name.equals(that.name)) return false; if (this.price != that.price) return false; return true; } public int hashCode() { int hash = 17; hash = 31 * hash + this.getId().hashCode(); hash = 31 * hash + this.getName().hashCode(); hash = 31 * hash + ((Double) this.getPrice()).hashCode(); return hash; } } 場合によっては、フィールドを Final にするだけでは十分ではありません。 たとえば、 MutableProductクラスはすべてのフィールドが Final ですが、不変ではありません。 なぜ上記のクラスは不変ではないのでしょうか? その理由は、クラスから参照を取得できるようにしているためです。「 カテゴリ」フィールドは変更可能な参照であるため、受信すると、クライアントはそれを変更できます。これを説明するために、次のプログラムを考えてみましょう。 そして、コンソール出力: 「カテゴリ」フィールドは変更可能であり、オブジェクトから取得されたため、クライアントはこのリストを変更しました。不変であるべきオブジェクトが変更され、新しい状態になりました。リストの内容を表現したい場合は、不変リスト表現を使用できます。 2. ステートレス オブジェクト ステートレス オブジェクトは不変オブジェクトに似ていますが、この場合、状態は 1 つもありません。オブジェクトがステートレス オブジェクトの場合、呼び出し間でデータを保持する必要はありません。状態が存在しないため、どのスレッドもオブジェクトのメソッドを呼び出して別のスレッドの結果に影響を与えることはできません。このため、ステートレス オブジェクトは本質的にスレッドセーフです。 ProductHandlerクラスは、このタイプのオブジェクトの例です。これには Product オブジェクトに対する複数の操作が含まれており、呼び出しの間にデータは保存されません。操作の結果は、以前の呼び出しや保存されたデータとは無関係です。 sumCart メソッドでは、ProductHandler が Product リストを配列に変換し、for-each ループで使用してすべての要素を反復処理します。反復子リストはスレッドセーフではないため、 反復中に変更があった場合は ConcurrentModificationExceptionをスローする可能性があります。ニーズに応じて、別の 戦略を選択することもできます。 3. スレッド ローカル変数 スレッド ローカル変数は、スレッド内で定義される変数です。他のスレッドはそれらを認識せず、変更しません。最初のタイプはローカル変数です。以下の例では、変数 total がスレッドのスタックに保存されます。 public final class MutableProduct { private final String id; private final String name; private final double price; private final List categories = new ArrayList<>(); public MutableProduct(String id, String name, double price) { this.id = id; this.name = name; this.price = price; this.categories.add("A"); this.categories.add("B"); this.categories.add("C"); } public String getId() { return this.id; } public String getName() { return this.name; } public double getPrice() { return this.price; } public List getCategories() { return this.categories; } public List getCategoriesUnmodifiable() { return Collections.unmodifiableList(categories); } public String toString() { return new StringBuilder(this.id).append("-").append(this.name) .append(" (").append(this.price).append(")").toString(); } } public static void main(String[] args) { MutableProduct p = new MutableProduct("1", "a product", 43.00); System.out.println("Product categories"); for (String c : p.getCategories()) System.out.println(c); p.getCategories().remove(0); System.out.println("\nModified Product categories"); for (String c : p.getCategories()) System.out.println(c); } Product categories A B C Modified Product categories B C public List getCategoriesUnmodifiable() { return Collections.unmodifiableList(categories); } public class ProductHandler { private static final int DISCOUNT = 90; public Product applyDiscount(Product p) { double finalPrice = p.getPrice() * DISCOUNT / 100; return new Product(p.getId(), p.getName(), finalPrice); } public double sumCart(List cart) { double total = 0.0; for (Product p : cart.toArray(new Product[0])) total += p.getPrice(); return total; } } public double sumCart(List cart) { double total = 0.0; for (Product p : cart.toArray(new Product[0])) total += p.getPrice(); return total; } プリミティブ変数の代わりに参照を定義してそれを返すと、その境界を離れることになることに注意してください。返されたリンクがどこで作成されたかわからない場合があります。 sumCartメソッドを呼び出すコードは、このメソッドを静的フィールドに保存し、別のスレッドからアクセスできるようにすることができます。2 番目のタイプは ThreadLocalクラスです。このクラスは、スレッドごとに独立したストレージを提供します。ThreadLocal に格納された値は、同じスレッド上のすべてのコードで使用できます。ClientRequestId クラスは、ThreadLocale クラスの使用例を示しています。 ProductHandlerThreadLocal クラスは、ClientRequestId を使用して、同じスレッド上で同じ生成された ID を返します。 main メソッドを実行すると、コンソール出力にはスレッドごとに異なる ID が表示されます。例: ThreadLocale を使用する場合は、(サーバー アプリケーションの場合のように) スレッドをマージするときに、いくつかの使用上のリスクに注意する必要があります。リクエスト間でメモリ リークや情報リークが発生する可能性があります。これについてはあまり詳しく説明しません。なぜなら... 記事「 ThreadLocale で自分の足を撃つ方法」は、これがどのようにして起こるかをよく示しています。 4. 同期の使用 オブジェクトへのスレッドセーフなアクセスを提供するもう 1 つの方法は、同期を使用することです。参照へのすべてのアクセスを同期すると、一度に 1 つのスレッド オブジェクトだけが参照にアクセスします。これについては今後の投稿で説明します。 5. 結論 複数のスレッドからアクセスできる単純なオブジェクトを構築できるいくつかの方法を検討しました。オブジェクトが複数の状態を持つ可能性がある場合、マルチスレッド エラーを防ぐことはさらに困難になります。一方、オブジェクトが 1 つの状態しか持てない場合、または状態を持たない場合は、複数のスレッドが同時にオブジェクトにアクセスすることを心配する必要はありません。オリジナルは こちらです public class ClientRequestId { private static final ThreadLocal id = new ThreadLocal () { @Override protected String initialValue() { return UUID.randomUUID().toString(); } }; public static String get() { return id.get(); } } public class ProductHandlerThreadLocal { //Same methods as in ProductHandler class public String generateOrderId() { return ClientRequestId.get(); } } T1 - 23dccaa2-8f34-43ec-bbfa-01cec5df3258 T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527d T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527d T3 - 126b8359-3bcc-46b9-859a-d305aff22c7e ...
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION