欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

<进阶-3> 对象的组合

程序员文章站 2022-07-12 18:42:15
...
[...续上文]
3. 对象的组合
前面我们已经介绍了关于线程安全和同步的一些基础知识。然而,我们并不希望每次内存访问都进行分析以确保程序是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或程序。下面介绍的一些组合模式,能够使一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。

3.1 设计线程安全的类
在线程安全的程序中,虽然可以将程序的所有状态都保存在共有的静态域中,但与将状态封装起来的程序比,还是后者的安全性更容易得到验证,并且前者在修改时更难以始终确保线程安全性。通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。
在设计线程安全类的过程中,需要同时包含三个基本要素:
1. 找出构成对象状态的所有变量。
2. 找出约束状态变量的不变性条件
3. 建立对象状态的并发访问管理策略。

3.2 实例封闭
如果对象不是线程安全的,可以通过各种技术使其在多线程程序中安全地使用。可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。
(可以对比上一篇2.3节的线程封闭)。
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(Instance Confinement)。当一个对象被封装在另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更易于对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
示例:
public class PersonSet
{
  private final Set<Person> mySet = new HashSet<Person>();

  public synchronized void addPerson(Person p)
  {
      mySet.add(p);
  }

  public synchronized boolean containsPerson(Person p)
  {
      return mySet.contains(p);
  }
}


这里是把Person对象封装在PersonSet中,保证对PersonSet的访问是线程安全的,不管Person类是否线程安全。但如果要线程安全的访问Person对象时,就需要额外的同步,或者将Person类也变成线程安全的类。比如自己在创建自己使用的简单缓存类时就可以选择这种方式。

实例封闭是构成线程安全类的一个最简单方式,它还使得在锁策略的选择上有更多的灵活性。
在java平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类,如Collections.synchronizedList及其类似方法。这些工厂方法通过“装饰器”模式将容器类封装在一个同步的包装器对象中。

3.2.1 java监视器模式
java监视器模式仅仅是一种编写代码的约定,对任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。如:
public class PrivateLock
{
    private final Object myLock = new Object();

    Widget widget;

    void someMethod()
    {
        synchronized(myLock)
        {
            //访问或修改Widget的状态
        }
    }
}

使用私有的锁对象而不是对象的内置锁(或任何其他可通过共有方式访问的锁)有许多优点,私有的锁对象可以将锁封装起来,使客户代码无法得到锁。

3.3 线程安全性的委托
大多数对象都是组合对象。当从头开始创建一个类,或将多个非线程安全的类组合成一个类时,java监视器模式非常有用。但是,如果类中的各个组件都已经是线程安全的,会是什么情况呢?是否需要增加一个额外的线程安全层?答案是“视情况而定”。

前面<进阶-1>篇1.2.2节的例子,CountingFactorizer类中,我们在一个无状态的类中增加了一个AtomicLong的域,并且得到的组合对象是线程安全的。由于CountingFactorizer的状态就是AtomicLong的状态,而AtomicLong是线程安全的,因此CountingFactorizer不会对counter的状态施加额外的有效性约束,所以很容易知道CountingFactorizer是线程安全的。我们可以说CountingFactorizer将它的线程安全性委托给AtomicLong来保证。

3.3.1 独立的状态变量
前面的委托示例仅仅委托给了单个线程安全的状态变量。我们还可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会再其包含的多个状态变量上增加任何不变性条件。

3.3.2 委托失效时
大多数组合对象都不会像上面那么简单,在他们的状态变量之间会存在某些不变性条件,比如两个值一个必须比另一个小,等,这些就存在先检查后执行的复合操作。这时,类本身必须提供自己的加锁机制以确保这些复合操作都是原子操作。

3.3.3 发布底层的状态变量
当把线程安全性委托给某个对象的底层状态变量时,要发布这些变量必须谨慎分析。如果一个状态变量是线程安全的,并且没有任何不变性条件约束他的值,在变量的操作上也不存在任何不允许的转换,那么就可以安全地发布这个变量。

3.4 在现有的线程安全类中添加功能
Java类库中包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类。但有时候某个现有的线程安全类只能支撑我们大部分的操作,此时就需要在不破坏线程安全性的情况下添加一个新的操作。

假如我们需要一个线程安全的链表,提供一个原子的“若没有则添加(put-if-absent)”的操作。同步的list类已经实现了大部分功能,但没有putIfAbsent(一些并发包里的集合是有这个操作的)。
一,要添加一个新的原子操作,最安全的方法是修改原始的类,但这通常无法做到,可能无法修改类的源码。
二,另一种方法是扩展这个类,假定可以继承这个类。排除一些类无法被继承,这种方法仍然比修改源码更加脆弱。因为现在的同步策略实现被分布在多个源码文件中,如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏。

3.4.1 客户端加锁机制
拿Collections.synchronizedList封装的ArrayList为例,上面的两种方法都不可行。因为客户代码并不知道在同步封装器工厂方法中返回的list对象的类型。这时可用第三种策略,通过“辅助类”扩展类的功能。
正确的做法:
public class ListHelper<E>
{
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    public boolean putIfAbsent(E x)
    {
        synchronized (list)
        {
            boolean absent = !list.contains(x);
            
            if (absent)
            {
                list.add(x);
            }
        
             return absent;
         }
     }
}


容易误导的错误做法:
public class ListHelper<E>
{
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    public synchronized boolean putIfAbsent(E x)
    {
        boolean absent = !list.contains(x);
        
        if (absent)
        {
            list.add(x);
        }
 
        return absent;
    }
}

为什么这种方式不安全呢?问题是在错误的锁上进行了同步,这并不是list本身需要的锁。

客户端加锁是脆弱的,它将对原始类的加锁代码放到与类完全无关的其他类(辅助类或使用类)中。客户端加锁机制与继承类机制有许多共同点,二者都将派生类的行为与基类的实现耦合在一起。

3.4.2 组合
当为现有的类添加一个原子操作时,一个更好的方法:组合。
public class ImprovedList<T> implements List<T> 
{
    private final List<T> list;

    public ImprovedList(List<T> list)
    {
        this.list = list;
    }

    public synchronized boolean putIfAbsent(T x)
    {
        boolean absent = !list.contains(x);
     
        if (absent)
        {
            list.add(x);
        }
     
        return absent;
    }

    public synchronized void clear()
    {
        list.clear();
    }
}


看完之后我们发现,其实设计模式里也是推崇组合代替继承,比如代理模式,装饰模式等。