java 并发 线程安全_Java并发教程–线程安全设计
java 并發 線程安全
在回顧了處理并發程序時的主要風險(例如原子性或可見性 )之后,我們將通過一些類設計來幫助我們防止上述錯誤。 其中一些設計導致了線程安全對象的構造,從而使我們能夠在線程之間安全地共享它們。 作為示例,我們將考慮不可變和無狀態的對象。 其他設計將防止不同的線程修改相同的數據,例如線程局部變量。
您可以在github上查看所有源代碼。
1.不可變的對象
不可變的對象具有狀態(具有表示對象狀態的數據),但是它是基于構造構建的,一旦實例化了對象,就無法修改狀態。
盡管線程可以交錯,但是對象只有一種可能的狀態。 由于所有字段都是只讀的,因此沒有一個線程可以更改對象的數據。 因此,不可變對象本質上是線程安全的。
產品顯示了一個不變類的示例。 它在構造期間建立所有數據,并且其任何字段均不可修改:
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類不是不可變的:
public final class MutableProduct {private final String id;private final String name;private final double price;private final List<String> 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<String> getCategories() {return this.categories;}public List<String> getCategoriesUnmodifiable() {return Collections.unmodifiableList(categories);}public String toString() {return new StringBuilder(this.id).append("-").append(this.name).append(" (").append(this.price).append(")").toString();} }為什么以上類別不是一成不變的? 原因是我們讓引用脫離了其類的范圍。 字段“ category ”是一個可變的引用,因此在返回它之后,客戶端可以對其進行修改。 為了顯示此,請考慮以下程序:
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 categoriesABCModified Product categoriesBC由于類別字段是可變的,并且逃脫了對象的范圍,因此客戶端已修改類別列表。 該產品原本是一成不變的,但已經過修改,從而進入了新的狀態。
如果要公開列表的內容,可以使用列表的不可修改視圖:
public List<String> getCategoriesUnmodifiable() {return Collections.unmodifiableList(categories); }2.無狀態對象
無狀態對象類似于不可變對象,但是在這種情況下,它們沒有狀態,甚至沒有一個狀態。 當對象是無狀態的時,它不必記住兩次調用之間的任何數據。
由于沒有修改狀態,因此一個線程將無法影響另一線程調用對象操作的結果。 因此,無狀態類本質上是線程安全的。
ProductHandler是此類對象的示例。 它包含對Product對象的多項操作,并且在兩次調用之間不存儲任何數據。 操作的結果不取決于先前的調用或任何存儲的數據:
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<Product> cart) {double total = 0.0;for (Product p : cart.toArray(new Product[0])) total += p.getPrice();return total;} }在其sumCart方法,所述ProductHandler產品列表轉換成一個陣列,因為for-each循環通過它的元件使用的迭代器內部進行迭代。 列表迭代器不是線程安全的,如果在迭代過程中進行了修改,則可能引發ConcurrentModificationException 。 根據您的需求,您可以選擇其他策略 。
3.線程局部變量
線程局部變量是在線程范圍內定義的那些變量。 沒有其他線程會看到或修改它們。
第一種是局部變量。 在下面的示例中, total變量存儲在線程的堆棧中:
public double sumCart(List<Product> cart) {double total = 0.0;for (Product p : cart.toArray(new Product[0])) total += p.getPrice();return total; }只要考慮一下,如果您定義引用并返回它,而不是原始方法,它將逃避其范圍。 您可能不知道返回的引用存儲在哪里。 調用sumCart方法的代碼可以將其存儲在靜態字段中,并允許在不同線程之間共享。
第二種類型是ThreadLocal類。 此類為每個線程提供獨立的存儲。 可以從同一線程內的任何代碼訪問存儲在ThreadLocal實例中的值。
ClientRequestId類顯示ThreadLocal用法的示例:
public class ClientRequestId {private static final ThreadLocal<String> id = new ThreadLocal<String>() {@Overrideprotected String initialValue() {return UUID.randomUUID().toString();}};public static String get() {return id.get();} }ProductHandlerThreadLocal類使用ClientRequestId在同一線程內返回相同的生成ID:
public class ProductHandlerThreadLocal {//Same methods as in ProductHandler classpublic String generateOrderId() {return ClientRequestId.get();} }如果執行main方法,則控制臺輸出將為每個線程顯示不同的ID。 舉個例子:
T1 - 23dccaa2-8f34-43ec-bbfa-01cec5df3258T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527dT2 - 936d0d9d-b507-46c0-a264-4b51ac3f527dT3 - 126b8359-3bcc-46b9-859a-d305aff22c7e...如果要使用ThreadLocal,則應注意在線程池化時使用它的一些風險(例如在應用程序服務器中)。 您可能會在請求之間導致內存泄漏或信息泄漏。 自從“ 如何用ThreadLocals射殺自己”一文很好地解釋了這種情況的發生之后,我將不再擴展本主題。
4.使用同步
提供對對象的線程安全訪問的另一種方法是通過同步。 如果我們將對引用的所有訪問同步,則在給定時間只有一個線程將訪問它。 我們將在后續帖子中對此進行討論。
5.結論
我們已經看到了幾種技術,可以幫助我們構建可以在線程之間安全共享的更簡單的對象。 如果一個對象可以具有多個狀態,則防止并發錯誤要困難得多。 另一方面,如果一個對象只能有一個狀態或沒有一個狀態,則不必擔心不同的線程同時訪問它。
翻譯自: https://www.javacodegeeks.com/2014/08/java-concurrency-tutorial-thread-safe-designs.html
java 并發 線程安全
總結
以上是生活随笔為你收集整理的java 并发 线程安全_Java并发教程–线程安全设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓版魔塔50层攻略(安卓版魔塔)
- 下一篇: java美元兑换,(Java实现) 美元