Java并发编程3:设计线程安全的类


作者: 康凯森

日期: 2016-11-27

分类: Java


1 设计线程安全的类

设计线程安全类的基本要素:

  • 找出构成对象状态的所有变量
  • 找出约束状态变量的不变性条件
  • 建立对象状态的并发访问管理策略

如果对象的所有域都是基本类型的变量,这些变量就构成了对象的全部状态。

如果对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。 例如 List的状态就包含列表中所有节点对象的状态。

由于不变性条件和后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装

如果在一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁。

当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时,Java监视器模式是非常有用的。

/**
 * Created by kangkaisen on 2016/11/27.
 * 使用java监视器模式的线程安全计数器
 */

@ThreadSafe
public class Counter {
    @GuardedBy("this")
    private long value = 0;

    public synchronized long getValue() {
        return value;
    }

    public synchronized long increment() {
        if (value == Long.MAX_VALUE) {
            throw new IllegalStateException("counter overflow");
        }
        return ++value;
    }
}

2 实例封闭

实例封闭是构建线程安全类的一个最简单方式

通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。

数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

/**
 * Created by kangkaisen on 2016/11/27.
 * 通过封闭机制来确保线程安全
 */
@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);
    }

}
/**
 * Created by kangkaisen on 2016/11/27.
 * 通过一个私有锁来保护状态.
 * 私有的锁对象可以将锁封装起来,使客户代码无法得到锁.
 */
@ThreadSafe
public class PrivateLock {
    private final Object myLock = new Object();

    @GuardedBy("myLock")
    Person person;

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

3 线程安全性的委托

如果一个类是由多个独立且线程安全的状态变量组成,并且在所有操作中都不包含无效的状态转换,就可以将线程安全性委托给底层的状态变量。(AtomicLongConcurrentHashMap 等)

如果某个类有复合操作,例如NumberRange ,那么仅靠委托并不足以实现线程安全性。此时,这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。

/**
 * Created by kangkaisen on 2016/11/27.
 * Bad case!
 * NumberRange 类并不足以保护它的不变性条件
 */
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 IllegalStateException("can't set lower, because it > upper");
        }
        lower.set(i);
    }

    public void setUpper(int i) {
        // 不安全的 "先检查后执行"
        if (i < lower.get()) {
            throw new IllegalStateException("can't set upper, because it < lower");
        }
        upper.set(i);
    }

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

如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。

/**
 * Created by kangkaisen on 2016/11/27.
 * 线程安全且可变
 */
@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;
    }
}

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

重用现有的类可以降低开发工作量,开发风险以及维护成本

在现有的线程安全类中添加功能有以下方法:

  1. 最安全的方法是修改原始的类,但通常无法做到。
  2. 扩展一个类,假设这个类在设计时考虑了可扩展性。
  3. 客户端加锁机制, 比较脆弱。
  4. 组合。 推荐!!!

客户端加锁是指:对于使用某个对象X的客户端代码,使用X本身保护其自身状态的锁在客户端保护这段代码。 所以必须知道对象X使用的是哪个锁。

通过组合实现在现有的线程安全类中添加功能的代码示例

/**
 * Created by kangkaisen on 2016/11/27.
 * 通过组合实现"若没有则添加"
 */

@ThreadSafe
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 contains = list.contains(x);

        if (!contains) {
            list.add(x);
        }

        return contains;
    }

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

    //按照类似的方式委托list的其他方法.
}

5 将同步策略文档化

在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。

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

6 参考资料

本文是《Java并发编程实战》的读书笔记。


评论