Java 并发编程之设计线程安全类

1.设计线程安全类

设计线程安全的类的步骤:
  1. 找出构成对象状态的所有变量
  2. 找出约束状态变量的先验条件,后验条件,不变性条件
  3. 建立对象状态的并发访问策略

2.实例封闭

封装简化了线程安全类的实现过程,提供了一种实例封闭的机制。当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。通过将封装机制和合适的加锁机制结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
将数据封装在对象内部,可以将数据的访问权限限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
从线程封闭原则可以得到Java监视器模式,遵循java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。
在许多现有的集合框架里面是有Java监视器模式,例如Vector和HashTable,但是Java监视器模式的主要优势在于其实现线程安全类的简单性,但是其并发操作的性能差。

3.线程安全性的委托

线程安全性的委托的意思是将一个类的线程安全性委托给自己的状态变量。
大多数对象都是组合对象,即对象的域中有其他对象。如果从头开始构建一个类,或者将多个非线程安全类组合成一个类时,可以考虑Java监视器模式。但是如果类中各个域都已经是线程安全的,还需要额外加一个额外的线程安全层吗?这时视情况而定。
如果多个域是相互独立的状态变量,则不需要额外线程安全层,即把类的安全性委托给了状态变量。
如果多个域共同参与维护对象的不变性条件,则不能将类的安全性直接委托给状态变量,可以通过加锁来维护不变性条件以保障其线程安全性。
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有操作中都不会包含无效的状态转换,那么可以将这个类的线程安全性委托给底层状态变量。
如果一个状态变量是线程安全的,并且不受任何不变性条的约束,而且在变量上的操作也不存在任何不允许的状态转换,这是可以安全地发布这个变量,否则不能发布这个变量,不正确的发布会导致类表现出超出设计者预期的状态。

4.在现有的线程安全类中添加功能

jdk中提供了很多好用的线程安全类,通常应该优先考虑现有的线程安全组件,而不是创建新类,如果现有的线程安全组件不能支持我们需要的操作,这是需要在不破坏组件线程安全性的情况下添加一个新的操作。

可以考虑的几种方法:
  1. 修改源码,在了解待修改的原始类的同步策略后,直接将新方法添加到类中,实现同步策略的所有代码任然在同一个源代码文件中,更易于维护。这是最安全的方式,但是通常无法做到。
  2. 扩展(extends)这个类。例如:
    public class BetterVector extends Vector {
        public synchronized boolean putIfAbsent(E e) {
            boolean absent = !contains(e);
            if (absent) {
                add(e);
            }
            return absent;
        }
    }
    扩展的方式很脆弱,因为现在同步策略实现被分布到多个独立维护的源码中,导致如果底层的类改变了同步策略并选择了不同的锁(显示锁或者new Object)来保护它的状态变量,以致于子类将会被破坏,因为子类无法在不改变源码的情况下使用正确的锁来控制对基类状态的并发访问。
  3. 客户端加锁机制。例如
    public class ListHelper {
        public List list = Collections.synchronizedList(new ArrayList());
    
        public boolean putIfAbsent(E e) {
            synchronized (list) {
                boolean absent = !list.contains(e);
                if (absent)
                    list.add(e);
                return absent;
            }
        }
    }
    客户端加锁是指对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护客户端代码。要使用客户端加锁,必须知道对象X使用的是哪一个锁。
    通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中,然而通过客户端加锁更加脆弱,因为它将类C的加锁代码放到了与C无关的其他类中。
    扩展方式破坏实现的封装性,客户端加锁破换同步策略的封装性。
  4. 组合。例如:
    public class ImprovedList implements List {
    
        private final List list;
    
        public ImprovedList(List list) {
            this.list = list;
        }
    
        public synchronized boolean putIfAbsent(T x) {
            boolean containts = list.contains(x);
            if (containts)
                list.add(x);
            return !containts;
        }
    
        //...按照类似方式委托List的其他方法
    }
    ImprovedList通过将List对象的操作委托给底层的List实例来实现List的操作,同时添加了一个原子的putIfAbsent方法。
    ImprovedList通过自身的内置锁增加了一层额外的加锁,它并不关心底层的List是否线程安全,即使List不是线程安全的或者修改了它的锁实现。虽然额外的同步层可能导致轻微的性能损失,但是与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。
总结:修改源码>组合>扩展>客户端加锁

你可能感兴趣的:(java并发)