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 categoriesABC
Modified 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 并发 线程安全