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;
    }
}

在某些情况下,将字段定为最终值还不够。 例如,尽管所有字段都是最终的,但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 categories

A

B

C
Modified Product categories

B

C

由于类别字段是可变的,并且逃脱了对象的范围,因此客户端已修改类别列表。 该产品原本是一成不变的,但已经过修改,从而进入了新的状态。

如果要公开列表的内容,可以使用列表的不可修改视图:

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>() {
        @Override
        protected String initialValue() {
            return UUID.randomUUID().toString();
        }
    };
    
    public static String get() {
        return id.get();
    }
}

ProductHandlerThreadLocal类使用ClientRequestId在同一线程中返回相同的生成ID:

public class ProductHandlerThreadLocal {
    //Same methods as in ProductHandler class
    
    public String generateOrderId() {
        return ClientRequestId.get();
    }
}

如果执行main方法,则控制台输出将为每个线程显示不同的ID。 举个例子:

T1 - 23dccaa2-8f34-43ec-bbfa-01cec5df3258

T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527d

T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527d

T3 - 126b8359-3bcc-46b9-859a-d305aff22c7e

...

如果要使用ThreadLocal,则应注意在线程池化时(例如在应用程序服务器中)使用它的一些风险。 您可能最终在请求之间出现内存泄漏或信息泄漏。 自从“ 如何与ThreadLocals一起开枪自杀”一文很好地解释了这种情况的发生之后,我将不再扩展本主题。

4.使用同步

提供对对象的线程安全访问的另一种方法是通过同步。 如果我们将对引用的所有访问同步,则在给定时间只有一个线程将访问它。 我们将在后续帖子中对此进行讨论。

5.结论

我们已经看到了几种技术,可以帮助我们构建可以在线程之间安全共享的更简单的对象。 如果一个对象可以具有多个状态,则防止并发错误要困难得多。 另一方面,如果一个对象只能有一个状态或没有状态,则不必担心不同的线程同时访问它。

翻译自: https://www.javacodegeeks.com/2014/08/java-concurrency-tutorial-thread-safe-designs.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目目目录 前言 第1章 计算机与网络安全基础 1 1.1 密码学与计算机安全 1 1.2 危险和保护 2 1.3 外围防护 3 1.3.1 防火墙 4 1.3.2 仅仅使用外围防护的不足之处 4 1.4 访问控制与安全模型 4 1.4.1 MAC和DAC模型 5 1.4.2 对数据和信息的访问 5 1.4.3 静态和动态模型 6 1.4.4 关于使用安全模型的几点考虑 6 1.5 密码系统的使用 7 1.5.1 单向散列函数 7 1.5.2 对称密码 8 1.5.3 非对称密码 9 1.6 鉴别 9 1.7 移动代码 10 1.8 Java安全性的适用范围 11 第2章 Java语言的基本安全特点 12 2.1 Java语言和平台 12 2.2 基本安全结构 13 2.3 字节代码验证和类型安全 14 2.4 签名应用小程序 15 2.5 关于安全错误及其修复的简要历史 16 第3章 JDK1.2安全结构 19 3.1 起源 19 3.2 为什么需要一个新型的安全结构 19 3.2.1 关于applet的沙盒模型的局限性 19 3.2.2 策略和实施分离的不彻底性 20 3.2.3 安全核查的不易扩展性 20 3.2.4 对本地安装的applet过于信任 20 3.2.5 内部安全机制的脆弱性 21 3.2.6 总结 21 3.3 java.Security.General SecurityException 21 3.4 安全策略 22 3.5 CodeSource 24 3.5.1 测试等同性和利用隐含 25 3.6 许可权层次 26 3.6.1 java.security.Permission 27 3.6.2 许可权集合 28 3.6.3 java.security.Unresolved Permission 29 3.6.4 java.io.FilePermission 31 3.6.5 java.net.SocketPermission 33 3.6.6 java.security.BasicPermission 35 3.6.7 java.util.PropertyPermission 36 3.6.8 java.lang.RuntimePermission 37 3.6.9 java.awt.AWTPermission 38 3.6.10 java.net.NetPermission 38 3.6.11 java.lang.reflect Reflect Permission 39 3.6.12 java.io .Serializable Permission 39 3.6.13 java.security.Security Permission 39 3.6.14 java.security.AllPermission 40 3.6.15 许可权隐含中的隐含 40 3.7 分配许可权 41 3.8 Protection Domain 42 3.9 安全地加载类 44 3.9.1 类加载器的层次 44 3.9.2 java.lang.ClassLoader和授权 46 3.9.3 java.security.SecureClassLoader 49 3.9.4 java.net.URLClassLoader 49 3.9.5 类的路径 50 3.10 java.lang.SecurityManager 51 3.10.1 使用安全管理器的实例 51 3.10.2 JDK1.2中没有改变的API 52 3.10.3 JDK1.2中禁用的方法 53 3.11 java.security.AccessController 56 3.11.1 AceessController的界面设计 57 3.11.2 基础访问控制算法 57 3.11.3 继承方法 59 3.11.4 扩展带有特权操作的基本算法 59 3.11.5 特权操作的三种类型 61 3.11.6 访问控制语境 63 3.11.7 完整的访问控制算法 64 3.11.8 SecurityManager与 AccessController 65 3.11.9 特权操作发展史 66 3.12 小结 67 第4章 安全结
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值