Java并发编程实战第四章笔记

2020-12-18  本文已影响0人  逍遥白亦

第四章 对象的组合

4.1 设计线程安全的类

在设计线程安全类的过程中,需要包含以下三个基本要素:

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

4.1.1 收集同步需求

假设不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就须要借助原子性和封装性。
说的更简略些是Java线程安全都是由于共享变量,共享变量后会由于多个线程同一时候改动导致不对的问题,所以收集一共同拥有多少处会涉及到这些须要同步的变量,仅仅有收集说有可能出问题的因素基于此之上保证全部元素线程安全也才干保证程序是线程安全的。

4.1.2 依赖状态的操作

类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的。

在某些方法中还包含一些基于状态的先验条件,例如:不能从空队列中删除元素,在单线程程序中,如果在空队列删除元素,只能失败;但是在多线程中,可以等队列中有元素了再删除,可以用阻塞队列Blocking Queue或信号Semaphore来实现

4.1.3 状态的所有权

4.2 实例封闭

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

被封闭对象一定不能超出它们既定的作用域。对象可以封闭在类的一个实例(例如作为类的一个私有成员)中,或者封闭在某个作用域内(例如作为一个局部变量),再或者封闭在线程内(例如在某个线程中将对象从一个方法传递到另一个方法,而不是在多个线程之间共享该对象)。

程序清单4-2

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

    interface Person {
    }
}

该程序说明了如何通过封闭与加锁等机制使一个类成为线程安全的。PersonSet的状态由HashSet管理,但是HashSet本身并不是线程安全的。但由于mySet是私有的,不会逸出,因此HashSet被封闭在PersonSet中。唯一能访问Myset的代码路径就是addPerson和containsPerson,又都加了锁。保证了线程安全。

Java类库中实例封闭的例子:Collections.synchronizedList及其类似方法

封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析累的线程安全性时就无须检查整个程序。

4.2.1 Java监视器模式

程序清单4-1

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

在Counter中封装了一个状态变量value,对该变量的所有访问都需要通过Counter的方法来执行,并且这些方法都是同步的。

下面程序给出了如何使用私有锁来保护状态
程序清单4-3

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

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

4.2.2 示例:车辆追踪

一个用于调度车辆的“车辆跟踪器”:使用监视器模式来构建车辆跟踪器,每台车都由一个String对象来标识,并且拥有一个相应的位置坐标(x,y)。在VehicleTracker类中封装了车辆的标识和位置。视图线程读取车辆的名字和位置,并将它们显示在界面上,执行更新操作的线程通过从GPS设备上获取的数据或者调度员从GUI界面上输入的数据来修改车辆的位置。视图线程与执行更新操作的线程将并发地访问数据模型,因此该模型必须是线程安全的。
程序清单4-4 基于监视器模式的车辆追踪

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

程序清单4-5 与Java.awt.Point类似的可变Point类

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

虽然MutablePoint不是线程安全的,但追踪器类是线程安全的。他所包含的Map对象和可变的Point对象都未曾发布。

4.3 线程安全性的委托

如果类中的各个组件都已经是线程安全的,在某些情况下是线程安全的,而在某些情况下,仅仅是一个开始。

4.3.1 示例:基于委托的车辆追踪器

构造一个委托给线程安全类的车辆追踪器。我们将车辆的位置保存到一个Map对象中,因此首先要实现一个线程安全的Map类,ConcurrentHashMap。我们还可以用一个不可变的Point类来代替MutablePoint以保存位置。

程序清单4-6

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

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

程序清单4-7

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

}

如果使用最初的MutablePoint类而不是Point类,就会破坏封装性,因为getLocations会发布一个对象的引用,而这个引用不是线程安全的。

如果需要一个不发生变化的车辆视图,那么getLocations只能返回对locations这个Map对象的浅拷贝,即返回线程安全对象的引用。

程序清单4-8

    public Map<String, Point> getLocationsAsStatic() {
        return Collections.unmodifiableMap(
                new HashMap<String, Point>(locations));
    }

4.3.2 独立的状态变量

到目前为止,这些委托示例都仅仅委托给了单个线程安全的状态变量。还可以将线程安全性委托给多个状态变量。

下面来看一个允许客户程序注册监控鼠标和键盘等事件的监听器。它为每种类型的事件都备有一个已注册监听器列表,因此当某个事件发生时,就会调用相应的监听器。然而,在鼠标事件监听器与键盘事件监听器之间不存在任何关联,二者是独立的,因此VisualComponent可以将其线程安全性委托给这两个线程安全的监听器列表。
程序清单4-9

public class VisualComponent {
    private final List<KeyListener> keyListeners
            = new CopyOnWriteArrayList<KeyListener>();
    private final List<MouseListener> mouseListeners
            = new CopyOnWriteArrayList<MouseListener>();

    public void addKeyListener(KeyListener listener) {
        keyListeners.add(listener);
    }

    public void addMouseListener(MouseListener listener) {
        mouseListeners.add(listener);
    }

    public void removeKeyListener(KeyListener listener) {
        keyListeners.remove(listener);
    }

    public void removeMouseListener(MouseListener listener) {
        mouseListeners.remove(listener);
    }
}

VisualComponent使用CopyOnWriteArrayList来保存各个监听器列表。它是一个线程安全的链表,特别适用于管理监听器列表。

4.3.3 当委托失效时

程序清单4-10

public class NumberRange {
    // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i) {
        // Warning -- unsafe check-then-act
        if (i > upper.get())
            throw new IllegalArgumentException("can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // Warning -- unsafe check-then-act
        if (i < lower.get())
            throw new IllegalArgumentException("can't set upper to " + i + " < lower");
        upper.set(i);
    }

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

NumBerRange使用了两个AtomicInteger来管理状态,并且有一个约束条件,lower小于等于upper。

但是NumBerRange并不是线程安全的,因为lower和upper不独立,会存在一个线程调用setLower(5),而另一个线程调用setUpper(4),而这两个操作都可以通过检查。

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

4.3.4 发布底层的状态变量

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

4.3.5 示例:发布状态的车辆追踪器

构造车辆追踪器的另一个版本,并在这个版本中发布底层的可变状态。要使用可变且线程安全的Point类。

程序清单4-11

@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.set(x, y);
    }

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

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

SafePoint提供的get方法同时获得x和y的值,并将二者放在一组中返回。

程序清单4-12

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

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

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

    public SafePoint getLocation(String id) {
        return locations.get(id);
    }

    public void setLocation(String id, int x, int y) {
        if (!locations.containsKey(id))
            throw new IllegalArgumentException("invalid vehicle name: " + id);
        locations.get(id).set(x, y);
    }
}

PublishingVehicleTracker将其线程安全性委托给底层的ConcurrentHashMap,但是Map中的Point是线程安全的可变对象。getLocation方法返回底层Map的一个不可变副本。调用者不能增加或删除车辆,但是可以改变SafePoint的值来改变车辆的位置。如果PublishingVehicleTracker没有对于车辆位置的约束,那么他就是线程安全的,反之就不安全。

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

尽量选择Java类库中已有的线程安全类来开发,如果已有类没有功能,可以在里边添加。

添加功能的两种方法

  1. 修改原始的类(一般不可能)
  2. 扩展这个类,比如Vector就可以扩展

程序清单4-13

@ThreadSafe
public class BetterVector <E> extends Vector<E> {
    // When extending a serializable class, you should redefine serialVersionUID
    static final long serialVersionUID = -3963416950630760754L;

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

4.4.1 客户端加锁机制

对于由Collections.synchronizedList封装的ArrayList,这两种办法都不行,那就只能扩展类的功能,将扩展代码放入一个辅助类里。

程序清单4-14

@NotThreadSafe
class BadListHelper <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使用哪一个锁来保护它的状态,这个锁并不是ListHelper上的锁。

我的理解是,list是public的已经发布出去了,可以被外部访问,就可以对这个list再加锁,但是无论怎么加锁,肯定不会是ListHelper里的锁。

正确做法如下
程序清单4-15

@ThreadSafe
class GoodListHelper <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;
        }
    }
}

4.4.2 组合

当为现有的类添加一个原子操作时,有一种更好的办法:组合。
程序清单4-16

@ThreadSafe
public class ImprovedList<T> implements List<T> {
    private final List<T> list;

    /**
     * PRE: list argument is thread-safe.
     */
    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;
    }

    // Plain vanilla delegation for List methods.
    // Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
    
    public int size() {
        return list.size();
    }

    public boolean isEmpty() {
        return list.isEmpty();
    }

    public boolean contains(Object o) {
        return list.contains(o);
    }

    public Iterator<T> iterator() {
        return list.iterator();
    }

    public Object[] toArray() {
        return list.toArray();
    }

    public <T> T[] toArray(T[] a) {
        return list.toArray(a);
    }

    public synchronized boolean add(T e) {
        return list.add(e);
    }

    public synchronized boolean remove(Object o) {
        return list.remove(o);
    }

    public boolean containsAll(Collection<?> c) {
        return list.containsAll(c);
    }

    public synchronized boolean addAll(Collection<? extends T> c) {
        return list.addAll(c);
    }

    public synchronized boolean addAll(int index, Collection<? extends T> c) {
        return list.addAll(index, c);
    }

    public synchronized boolean removeAll(Collection<?> c) {
        return list.removeAll(c);
    }

    public synchronized boolean retainAll(Collection<?> c) {
        return list.retainAll(c);
    }

    public boolean equals(Object o) {
        return list.equals(o);
    }

    public int hashCode() {
        return list.hashCode();
    }

    public T get(int index) {
        return list.get(index);
    }

    public T set(int index, T element) {
        return list.set(index, element);
    }

    public void add(int index, T element) {
        list.add(index, element);
    }

    public T remove(int index) {
        return list.remove(index);
    }

    public int indexOf(Object o) {
        return list.indexOf(o);
    }

    public int lastIndexOf(Object o) {
        return list.lastIndexOf(o);
    }

    public ListIterator<T> listIterator() {
        return list.listIterator();
    }

    public ListIterator<T> listIterator(int index) {
        return list.listIterator(index);
    }

    public List<T> subList(int fromIndex, int toIndex) {
        return list.subList(fromIndex, toIndex);
    }

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

ImprovedList通过自身的内置锁增加了一层额外的加锁。他并不关系底层的List是不是线程安全的。因为List并没有暴露出去。

4.5 将同步策略文档化

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

上一篇 下一篇

猜你喜欢

热点阅读