JavaRush /Java Blog /Random-TW /Java 中的並發。教程 - 線程安全建構。
0xFF
等級 9
Донецк

Java 中的並發。教程 - 線程安全建構。

在 Random-TW 群組發布
在了解了執行平行程序的主要風險(例如原子性可見性)之後,我們將了解一些有助於我們防止上述陷阱的類別設計。其中一些構造創建線程安全對象,使我們能夠在線程之間安全地共享它們。作為範例,我們將研究不可變和無狀態物件。其他視圖將阻止不同線程修改數據,例如線程局部變數。 您可以在Github 上查看所有原始碼。 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; } } 在某些情況下,將字段設置為最終字段是不夠的。 例如, MutableProduct類別不是不可變的,儘管所有欄位都是最終的: 為什麼上面的類別不是不可變的?原因是我們允許從類別中檢索引用。「 categories」欄位是一個可變引用,因此一旦收到,客戶端就可以對其進行修改。為了說明這一點,請考慮以下程序: 以及控制台輸出: 由於「categories」欄位是可變的並且是從物件獲取的,因此用戶端已修改此清單。一個應該不可變的物件已經被改變,導致一個新的狀態。如果要表示清單的內容,可以使用不可變列表表示: 2. 無狀態物件 無狀態物件與不可變物件類似,但在這種情況下,它們沒有狀態,甚至沒有狀態。當物件是無狀態物件時,它不必在呼叫之間保留任何資料。由於不存在狀態,因此任何執行緒都不能透過呼叫物件的方法來影響另一個執行緒的結果。因此,無狀態物件本質上是線程安全的。 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方法的程式碼可以將其儲存在靜態欄位中,並允許不同的執行緒存取它。第二種是 ThreadLocal類別。該類別為每個執行緒提供獨立的儲存。儲存在ThreadLocal中的值可供同一執行緒上的任何程式碼使用。ClientRequestId 類別顯示了 ThreadLocale 類別的使用範例: ProductHandlerThreadLocal 類別使用 ClientRequestId 在同一執行緒上傳回相同的產生 ID: 執行 main 方法時,控制台輸出將為每個執行緒顯示不同的 ID。例如: 如果您要使用ThreadLocale,則在合併執行緒時(如在伺服器應用程式中)必須注意一些使用風險。您可能會在請求之間發生記憶體洩漏或資訊洩漏。我不會對此進行過多討論,因為... 「如何用 ThreadLocale 搬起石頭砸自己的腳」一文很好地示範了這是如何發生的。 4. 使用同步 提供對物件的執行緒安全存取的另一種方法是透過同步。如果我們同步對一個引用的所有訪問,那麼在給定時間只有一個執行緒物件會訪問它。我們將在以後的帖子中討論這個問題。 5. 結論 我們研究了幾種允許您建立可由多個執行緒存取的簡單物件的方法。如果一個物件可以有多個狀態,那麼防止多執行緒錯誤就會困難得多。另一方面,如果一個物件只能有一個狀態或沒有狀態,我們就不必擔心多個執行緒同時存取該物件。原文 在這裡 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