3.组合对象

我们希望线程安全的组件能够以安全的方式组合成更大的组件或程序。

3.1设计线程安全的类

设计线程安全类的过程应该包括下面3个基本要素:

  • 确定对象状态是由哪些变量构成的

    • 对象就的状态就是由其内部所有的变量构成的
  • 确定限制状态变量的不变约束
  • 制定一个管理并发访问对象状态的策略

    • 策略定义的对象如何协调对其状态的访问。它规定了如何把不可变性、线程限制和锁结合起来。

例子:

  1. @ThreadSafe
  2. public final class Counter{
  3. @GuardedBy("this") private long value = 0;
  4. public synchronized long getValue(){
  5. return value;
  6. }
  7. public synchronized long increment(){
  8. if (value == Long.MAX_VALUE)
  9. throw new IllegalStateException("");
  10. return ++value;
  11. }
  12. }
  13. //这个类中,状态value和方法getValue()、increment()的锁都是对象本身。
  14. //也就是说,同一时刻只能有一个线程来访问这个类中的方法和变量。

3.1.1收集同步需求

对象与变量拥有一个状态空间

即它们可能处于的状态的范围。状态空间越小,越容易判断它们。

很多类通过不可变约束判定某一种状态是合法的还是非法的

例如上一小节中的Counter类中的value域是long类型。long的状态空间为Long.MIN_VALUE到Long.MAX_VALUE的范围。**但是Counter约束了value的取值:不允许为负值**。

value初始化时就为0,并且只能增加,当增加至MAX_VALUE时,就会抛出异常,所以不能为负。

操作的后验条件也会指出某种状态转换是非法的

例如:如果Counter当前状态是17,下一个唯一合法的状态就是18。如果**下一个状态源自当前状态**,那么这个**操作必须是复合操作**。

不是所有的操作都会受限于状态转化的约束,当更新一个表示温度的数值时,计算结果并不受先前状态的影响。

一个类的不变约束可以约束多个状态变量。这种多变量的不变约束,需要原子性

不理解对象的不变约束和后验条件,就不能保证线程的安全性。要约束状态变量的有效值或者状态转换,就需要原子性与封装性。

3.1.2状态依赖的操作

若一个操作存在基于状态的先验条件则把它称为是状态依赖的

例如:你无法从空队列中移除一个条目:在你删除元素前,队列必须处于“非空”状态。

在单线程中,操作如果无法满足先验条件,必然失败。

在并发程序中,原本为假的先验条件可能会**由于其它线程的活动而变成真**。可以**持续等待,直到先验条件为真,再继续处理操作**。

想要正确的使用wait和notify并不容易,所以不如使用现有类库来提供期望的状态依赖行为。比如:阻塞队列或信号量以及其它同步工具(synchronizer)。

3.2实例限制

将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁。

实例:

@ThreadSafe
public class PersonSet{
    @GuardedBy("this")
    private final Set<Person> mySet = new HashSet<>();

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

    public synchronized boolean containsPerson(Person p){
        return mySet.contains(p);
    }
}
限制性使构造线程安全的类变得更容易。因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序。

3.2.1Java监视器模式

线程限制原则的直接推论之一是**Java监视器模式**。

Java监视器模式仅仅是一种**习惯约定**:**任意锁对象,只要始终如一地使用,都可以用来保护对象的状态**。
public class PrivateLock{
    private final Object myLock = new Object();
    @GuardedBy("myLock") Widget widget;

    void someMethod(){
        synchronized(myLock){
            //访问或修改widget的状态
        }
    }
}
使用**私有锁对象**的好处:

    私有的锁对象可以封装锁。防止客户不正确的得到锁。如果是使用公共锁,验证时需要检查完整的程序,而不是一个单独的类。

3.3委托线程安全

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

实例:

//将线程安全委托到ConcurrentHashMap
@ThreadSafe
public class DelegatingVehicleTracker {
    private final ConcurrentMap<String, Point> locations;
    private final Map<String, Point> unmodifiableMap;

    public DelegatingVehicleTracker(Map<String, Point> points){
        locations = new ConcurrentHashMap<String, Point>(points);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }

    public Map<String, Point> getLocations(){
        return unmodifiableMap;
    }

    public Point getLocaltion(String id){
        return locations.get(id);
    }

    public void setLocations(String id, int x, int y){
        if (locations.replace(id, new Point(x, y)) == null)
            throw new IllegalArgumentException("");
    }
}

3.3.1当委托无法胜任时

当状态变量不是彼此独立时,是无法胜任委托的。

这时就不能简单地将线程安全性委托给线程安全的状态变量。还要通过加锁维护不变约束。

这种情况与volatile变量的一条规则类似:

当且仅当一个变量没有参与那些设计其它状态变量的不变约束时,才适合声明为volatile类型。

实例:

//变量的线程安全的,但是并不彼此独立,所以会出线程安全问题。
//可能会出现 upper < lower的情况。
public class NumberRange {
    //不变约束:lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i){
        //这是不安全的
        if (i > upper.get()){
            throw new IllegalArgumentException("");
        }
        lower.set(i);
    }

    public void setUpper(int i){
        //这是不安全的
        if (i > upper.get()){
            throw new IllegalArgumentException("");
        }
        upper.set(i);
    }

    public boolean isInRange(int i){
        return (i >= lower.get() && i <= upper.get());
    }
}

3.3.2发布底层的状态变量

如果一个状态变量是线程安全的,没有任何不变约束限制它的值,并且没有任何状态转换限制它的操作,那么它可以被安全的发布。

实例:

@ThreadSafe
public class PublishingVehicleTracker {
    private final ConcurrentMap<String, SafePoint> locations;
    private final Map<String, SafePoint> unmodifiableMap;

    public PublishingVehicleTracker(Map<String, SafePoint> points) {
        locations = new ConcurrentHashMap<String, SafePoint>(points);
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }

    public Map<String, SafePoint> getLocations() {
        return unmodifiableMap;
    }

    //发布了底层可变状态却不破换线程安全性
    public SafePoint getLocaltion(String id) {
        return locations.get(id);
    }

    public void setLocations(String id, int x, int y) {
        if (locations.replace(id, new Point(x, y)) == null)
            throw new IllegalArgumentException("");
    }
}


@ThreadSafe
public class SafePoint {
    @GuardedBy("this")
    private int x, y;

    private SafePoint(int[] a) {
        this(a[0], a[1]);
    }

    public SafePoint(SafePoint p) {
        this(p.get());
    }

    public SafePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public synchronized int[] get() {
        return new int[]{x, y};
    }

    public synchronized void set(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

3.4向已有的线程安全类添加功能

方法:扩展类、修改原始类

//缺少即加入
@ThreadSafe
public class BetterVector<E> extends Vector<E>{
    public synchronized boolean putIfAbsent(E x){
        boolean absent = !contains(x);
        if(absent){
            add(x);
        }
        return absent;
    }
}

3.4.1客户端加锁

有时候扩展类或者修改原始类都不好使用。(例如:想要在由Collections.synchronizedList封装的ArrayList中添加功能)

第三个策略:扩展功能,而不扩展类本身 。
//缺少即加入
@ThreadSafe
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){
                    add(x);
            }
            return absent;
        }
    }
}

3.4.2组合

向已有的类中添加一个原子操作,还有更健壮的选择:组合。

实例:

//缺少即加入
@ThreadSafe
public class ImprovedList<T> implements List<T> {
    private final List<E> list;

    public ImprovedList(List<T> list) {this.list = list;}
    public synchronized boolean putIfAbsent(E x){
            boolean absent = list.contains(x);
            if(absent){
                    add(x);
            }
            return !absent;
    }

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

只要我们的类持有底层list的唯一外部引用,就能保证提供线程安全性。

3.5同步策略的文档化

为类的用户编写类线程安全性担保的文档。

为类的维护者编写类的同步策略文档。

可以使用@GuardedBy标签。只有当线程有该注解标识的锁时,才能访问被该注解标识的变量。

如果一个类没有明确指明,就不要假设它是线程安全的。