Java Concurrency in Practice 读书笔记 第四章

第四章  组成(线程安全的)对象

4.1  设计线程安全的类

设计一个线程安全的类包含如下准则:

1、识别出哪些变量将改变类的状态

2、识别出约束状态变量的不变条件

3、建立起规则,用于管理并发访问状态的状态

如果一个对象的field都是由基本数据类型(int long等)组成的,则所有这些field就构成了对象的全部状态。

如果对象的field中还包含引用,则对象状态还要包括这些引用变量中的隐含数据。例如LinkedList中的Node。

为了方便后人阅读代码,应该在代码处对同步机制作出注释,特别是哪个类由哪个锁所保护,如下所示:

@ThreadSafe

public final 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;

    }

}

用标注的形式来表示这样:@GuardedBy("this")

4.1.1   收集同步需求

为了更好的了解同步需求,首先要根据field字段获得对象的状态空间。

这里面还要根据实际业务来确定,例如:

1、Long的范围是Long.MIN_VALUE到Long.MAX_VALUE。但对于计数器Counter而言,显然负数不是状态空间。

2、有的Field有后置条件:如果现在计数值是16,则下一个必须是17,不是所有的变量都有这种特性。

3、有些field是相互绑定的,比如对于Cache数、因子的例子,要么两个同时更新,要么不更新。更新一个忘了另一个是不符合对象的状态空间的。

4.1.2  状态相关的操作

除了静态层面上,还有一些对象由状态相关(State)的约束条件。

例如:从队列移除对象之前,队列必须是非空的。这类限定叫做状态相关的约束条件。

单线程条件下,不满足状态相关约束条件只能失败,但是并发程序中则不然。

线程等待的过程中,可能由于其他线程的作用,使约束条件变为有效。

关于等待条件为真的问题上,可由wait()和notify()加上内置锁来解决,但机制比较复杂难于实现。所以也可以用现成的类:例如BlockingQueue,Semphore。这些机制将在第5章进行介绍。

4.1.3  状态的所有权

Field的所有权不仅包含直接的field,还包含例如Map中的Map.Entry。对于这些Entry而言,其所有权是共享的。

举个例子:

ServletContext封装了类似Map的操作:setAttribute和getAttribute,ServletContext保证了其本身是线程安全的。也就是说get和set在调用时不需要额外的同步代码但是对于get得到的对象的操作,必须进行同步控制,因为很有可能还有其他线程在对这些对象进行操作。要么保证对象是线程安全的,或者保证是不变的,或者加锁控制。

4.2  实例限制

尽管组成类的Field可能不是线程安全的,但是通过“实例限制”的方法,可以让它组成的类编程线程安全的。

如下所示:

Person并不是线程安全的,但是mySet是private的,不会外泄且只能通过addPerson和containsPerson访问,而这两个操作又是被锁定的。

由此将并非线程安全的Person组成了线程安全的PersonSet

@ThreadSafe
public class PersonSet {
    @GuardedBy("this")
    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);
    }
}

尽管PersonSet是线程安全的了,但是如果Person是易变的,则最好也将Person变成线程安全的。

在JDK中,也使用了类似方法,将并非线程安全的ArrayList和HashMap变成了线程安全的Collections.synchronizedList等。通过包装的设计模式,将非线程的原始类限定在新类之内。包装方法(摘自JDK):

List list = Collections.synchronizedList(new ArrayList());
...
synchronized(list) {
Iterator i = list.iterator(); // Must be in synchronized block
while (i.hasNext())
foo(i.next());
}
注意尽管包装后的List是线程安全的了,但是迭代器遍历时候仍然要同步。

4.2.1  Java监视器模式

Java监视器是另一种线程安全的设计模式:

public class PrivateLock {
    private final Object myLock = new Object();
    @GuardedBy("myLock") Widget widget;

    void someMethod() {
        synchronized(myLock) {
            // Access or modify the state of widget
        }
    }
}

它自己创建了一个Object作为锁。Vector、Hashtable等都使用了类似的设计模式。使用这种私有锁而不是将对象作为锁(synchronized方法)的好处是:使得外界无法直接参与作用同步机制,防止了意外的发生。

来看一个例子:

@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this")
private final Map<String, MutablePoint> locations;

public MonitorVehicleTracker(
Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}

public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}

public synchronized  MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}

public synchronized  void setLocation(String id, int x, int y) {
MutablePoint loc = locations.get(id);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.y = y;
}

private static Map<String, MutablePoint> deepCopy(
Map<String, MutablePoint> m) {
Map<String, MutablePoint> result =
new HashMap<String, MutablePoint>();
for (String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}

@NotThreadSafe
public class MutablePoint {
    public int x, y;

    public MutablePoint() { x = 0; y = 0; }
    public MutablePoint(MutablePoint p) {
        this.x = p.x;
        this.y = p.y;
    }
}

可以注意到,MonitorVehicleTracker是一个线程安全的类。然而由于MutablePoint不是线程安全的,所以在从这个类返回的任何包含MutablePoint类都是经过拷贝的副本。这是常用的一个技巧,否则必须在程序所有地方对MutablePoint操作的时候加上同步控制。

4.3  授权线程安全

Java监视器模式可以很好地解决由非线程安全field构成的对象的并发访问问题。但对于field都是线程安全的情况,有时并不必这么做。

改用线程安全的点类(因为不可变所以线程安全):

@Immutable
public class Point {
    public final int x, y;

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

线程安全的这个Point可以随意被发布,也不用再return之前被拷贝。

这样之后,DelegatingVehichleTracker也不用再做同步控制了,因为ConcurrentMap保证了线程安全!!

@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 getLocation(String id) {
        return locations.get(id);
    }

    public void setLocation(String id, int x, int y) {
        if (locations.replace(id, new Point(x, y)) == null)
            throw new IllegalArgumentException(
                "invalid vehicle name: " + id);
    }
}

4.4 向线程安全的类添加函数

有的时候,复用已有的类可以大大简化我们的工作量,尤其是线程安全的类。但他们往往缺少我们所需要的功能,因此可以拓展线程安全的类并添加自己需要的方法。

例如,我们需要List提供“没有则插入”的功能,一个很好的做法就是拓展已然是线程安全的Vector。

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

直接使用vector提供的contains和add方法即可完成。但这两个操作不在一个原子操作里,因此必须用锁控制。否则可能出现插入两次的情况。

但是,并非所有时候都能很方便的进行extends,有时候我们需要构造一个Helper类来代理。

比如对于Collections.synchronizedList包装的ArrayList,客户端无法知道原来被包装的是ArrayList,更无从extends。

因此需要Helper。

下面是一个错误的例子:

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

看出问题在哪里了吧?没错,使用了错误的锁,synchronized使用的是this,锁定this可不好用啊,因为操作是在list上发生的!因此应该改用下面的锁定方法:

@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)
                list.add(x);
            return absent;
        }
    }
}

当然Helper实现的并不优雅,因为在使用两个“逻辑锁”(List自身的this和ListHelper中的list)指向同一个“物理对象锁”。

因此也可以自己将欲拓展的类包装起来:

@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(); }
    // ... similarly delegate other List methods
}

4.5  对同步策略进行注释

对各种同步措施,无论是synchronized、volatile还是其他线程安全的class进行注释是非常必要的。

@GuardedBy是不错的一种选择。

然而比较悲剧的是,目前即使Java SE6中的Class,也很少对线程安全进行注释。

一种方法是猜测,或者询问开发者。

另一种方法是,根据他的用途/功能猜测。

例如DataSource为了获取多个Connection,有谁会没事闲的在一个线程反复获取多个Connection呢?显然是为并发设计的,所以应该是线程安全的。

相反,Connection没有理由被多线程共享(SQL查询会被中断!),所以不是线程安全的。

但我觉得最靠谱儿的方法还是拿来去实验一下,跑些高压力的TestCase来看看。

Leave a Reply

Your email address will not be published. Required fields are marked *