JavaRush /Java 博客 /Random-ZH /Java 中的并发。教程 - 线程安全构造。
0xFF
第 9 级
Донецк

Java 中的并发。教程 - 线程安全构造。

已在 Random-ZH 群组中发布
在了解了运行并行程序的主要风险(例如原子性可见性)之后,我们将了解一些有助于我们防止上述陷阱的类设计。其中一些构造创建线程安全对象,使我们能够在线程之间安全地共享它们。作为示例,我们将研究不可变和无状态对象。其他视图将阻止不同线程修改数据,例如线程局部变量。 您可以在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