并发编程----对象的组合


4.转载地址https://blog.csdn.net/ahaha413525642/article/details/76617019

1 设计线程安全的类

在设计线程安全类的过程中,需要包含下面三个基本要素
①找出构成对象状态的所有量
②找出约束状态变量的不变性条件
③建立对象状态的并发访问管理策略

要访问对象的状态,首先从对象的域开始。如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。

Counter只有一个域value,因此这个域就是Counter的全部状态。

//             4-1    使用java监视器模式的线程安全计数器
@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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

对于含有n个基本类型的对象,其状态就是这些域构成的n元组。例如,二维点的状态就是它的坐标值(x,y),如果在对象的域中引用了其他对象,那么该对象的状态包含被引用对象的域。例如:LinkedList的状态就包含该链表中所有节点对象的状态。

同步策略(Synchronized Policy)定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作进行协同。

4.1.1 收集同步需求(Gathering Synchronization Requirements)

不可变条件,用来判断状态是否是有效的
要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏,这就需要对其状态进行推断。对象与变量都有一个状态空间,即所有可能的值。状态空间越小,就越容易判断线程的状态。final类型的域使用的越多,越能简化对象可能状态的分析过程。

在多数类中都定义了一些不可变条件,用来判断状态是否是有效的。Counter中的value域是long类型的变量,其状态空间从Long.MIN.VALUE到Long.MAX.VALUE,但不能为负值。

后验条件,来判断状态迁移是否是有效的
如果Counter的当前值为17,那么下一个有效状态只能是18.当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作(读取-修改-写入)。并非所有的操作都会在状态转换上施加限制,例如,当更新一个保存当前温度的变量时,该变量之前的状态并不会影响计算结果。

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

在类中也可以包含同时约束多个状态变量的不变性条件。例如,在一个表示数值范围的类中可以包含两个状态变量,分别表示范围的上界与下界。

类似与这种包含多个变量的不变性条件必将带来原子性需求:这些相关的变量必须在单个原子操作中进行读取或更新。

如果不了解对象的不变性条件与后验条件,就不能确保线程安全性。要满足在状态变量的有效值或状态转移上的各种约束条件,就需要借助原子性与封装性。

4.1.2 依赖状态的操作(State-dependent Operations)

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

某些对象的方法中还包含一些基于状态的先验条件(state-based preconditions)
例如,不能从空队列中移除一个元素;在删除元素前,队列必须处于非空状态。

如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作

在单线程程序中,如果某个操作无法满足先验条件,就只能失败。但在并发程序中,先验条件可能会由于其他线程执行的操作而变成成,在并发线程中要一直等到先验条件为真,然后再执行该操作。

在java中,通过现有库中的类(例如阻塞队列【Blocking Queue】或信号量【Semaphore】)来实现依赖状态的行为。

4.1.3 状态的所有权(State Ownership)

如果以某个对象为根节点来构造一张对象图,该对象的状态将是对象图中所有对象包含的域的一个子集。

在定义哪些变量将构成对象的状态时,只考虑对象拥有的数据。所有权(Ownership)属于类设计中的一个要素。如果分配并填充了一个HashMap对象,就相当于创建了多个对象:HashMap对象,在HashMap对象包含的多个对象,以及在Map.Entry中可能包含的内部对象。HashMap对象的逻辑状态包括所有的Map.Entry对象以及内部对象,即使这些对象都是一些独立的对象。

Java的垃圾回收机制使我们避免了如何处理所有权的问题。

多数情况下,所有权与封装性总是互相关联的:对象封装它拥有的状态,对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。
然而,如果发布了某个可变对象的引用,就不再拥有独占的控制权,最多是“共享控制权”。对于从构造函数或从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象的所有权(如同步容器封装器的工厂方法)。

容器类通常表现出一种“所有权分离”的形式,其中容器类拥有其自身的状态,而客户代码则拥有容器中各个对象的状态。


4.2 实例封闭

如果某对象不是线程安全的,要在多线程程序中安全使用,你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。

封装简化了线程安全类的实现过程,它提供了一种实例封闭(Instance Confinement)机制,简称“封闭”。当一个对象被封装到另一个对象中,能够访问被封装对象的所有代码路径都是一致的。

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

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

PersonSet说明了如果通过封闭与加锁等机制来使类成为线程安全的(即使这个类的状态变量不是线程安全的)。
PersonSet的状态(可以理解为成员属性)由HashSet来管理的,而HashSet并非线程安全的,但由于mySet是私有的并且不会逸出,因此HashSet被封闭在PersonSet中。唯一能访问mySet的代码路径是addPerson与containsPerson,在执行它们它们时都要获得PersonSet上的锁(this).PersonSet的状态完全由它的内置锁保护,因此PersonSet是一个线程安全的类。

//        4-2     通过封闭机制来确保线程安全
@ThreadSafe
public class PersonSet {
    @GuardedBy("this") private final Set<Person> mySet = new HashSet<Person>();//HashSet并非线程安全的,但由于mySet是私有的并且不会逸出,因此HashSet被封闭在PersonSet中

    public synchronized void addPerson(Person p) { //在执行它们它们时都要获得PersonSet上的锁
        mySet.add(p);
    }

    public synchronized boolean containsPerson(Person p) {//在执行它们它们时都要获得PersonSet上的锁
        return mySet.contains(p);
    }

    interface Person {
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这个实例并未对Person的线程安全性做任何假设,但如果Person类是可变的,那么在访问从PersonSet中获得的Person对象时,还需要额外的同步。要想安全地使用Person对象,最可靠的方法是使Person成为一个线程安全的类。另外也可以使用锁来保护Person对象,并确保所有客户代码在访问Person对象之前都已经获得正确的锁。

实例封闭是构建线程安全类的一个最简单的方式,它使用在锁策略的选择上拥有了更多的灵活性。实例封闭还使得不同的状态变量可以由不同的锁来保护。

如果将一个本该被封闭的对象发布出去,会破坏封闭性。

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

4.2.1 Java监视器模式(The Java Monitor Pattern)

JAVA每个对象(Object/class) 都关联一个监视器,更好的说法应该是每个对象(Object/class)都有一个监视器,对象可以有它自己的临界区,并且能够监视线程序列为了使线程协作。

进入和退出同步代码块的字节指令也称为monitorenter和monitorexit,而Java的内置锁也称为监视器锁或监视器。

为了使数据不被多个线程访问,java 提供了同步块 以及 同步方法两种实现,一旦一段代码被嵌入到一个synchronized关键字中,意味着放入了监视区域,JVM在后台会自动为这段代码实现锁的功能。

遵循Java监视器模式的对象会把对象的所有可变状态都封装起来。

Java监视器模式的主要优势就在于它的简单性。

Java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。如下:

//        4-3   通过一个私有锁来保护状态
public class PrivateLock {
    private final Object myLock = new Object();
    @GuardedBy("myLock") Widget widget;

    void someMethod() {
        synchronized (myLock) {
            // 访问或修改Widget的状态
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

使用私有的锁对象而不是对象的内置锁(或其他任何可通过公有方式访问的锁),有许多优点:
私有的锁对象可将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有方法来访问锁,以便参与到它的同步策略中。

4.2.2 车辆追踪

上面的Counter是简单的Java监视器模式示例,下面我们来看一个别的示例:
一个用于调度车辆的“车辆追踪器”。首先使用监视器模式来构建车辆追踪器,然后再蚕食放宽某写封装性需求同时又保持线程安全性。

//     4-4   基于监视器模式的车辆追踪
@ThreadSafe
 public class MonitorVehicleTracker {
    @GuardedBy("this") private final Map<String, MutablePoint> locations;  
    //每辆车的信息,包括一个String标识每一辆车,MutablePoint为位置

    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) {//根据id返回位置
        MutablePoint loc = locations.get(id);
        return loc == null ? null : new MutablePoint(loc);//如果loc==null就返回null,否则返回得到二维坐标
    }

    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);//返回的映射是只读的。试图对返回的映射进行修改会报异常。不关是直接修改还是通过视图引用修改
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

其中的MutablePoint来表示车辆的位置,但是不是线程安全的。因为x,y不是final类型,是可变的。

//    4-5   线程不安全的可变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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

虽然MutablePoint不是线程安全的,但是追踪器类是线程安全的。它所包含的Map队形和可变的Point对象都未曾发布。当需要返回车辆的位置时,通过MutablePoint拷贝构造函数或者deepCopy来赋值正确的值,从而生成一个新的Map对象,并且该对象中ed值与原有的Map对象中的key和value相同。

在某种程度上,这种实现方式是通过在返回客户代码之前复制可变的数据来维持线程安全性的。在车辆容器非常大的情况下将极大地降低性能(由于deepCopy是从一个synchronized方法中调用的,因此在执行较长事件的复制操作中,tracker的内置锁将一直被占有)。

此外,由于每次调用getLocation就要复制数据,因此将出现一种错误情况:虽然车辆的实际位置发生了变化,但返回的信息不变。


4.3 线程安全性的委托(Delegating Thread Safety)

在某些情况下,通过多个线程安全类组合而成的类是线程安全的,而在某些情况下,这仅仅是一个好的开端。

在前面的CountingFactorizer类中(第二章),我们在一个无状态的类中增加了一个AtomicLong类型的域(count),并且得到的组合对象仍是线程安全的。由于CountingFactorizer的状态就是AtomicLong的状态,而AtomicLong是线程安全的,因此CountingFactorizer不会count的状态施加额外的有效性约束,所以易知CountingFactorizer是线程安全的。

我们可以说CountingFactorizer将它的线程安全性委托给AtomicLong来保证:因为AtomicLong是线程安全的,所以CountingFactorizer是线程安全的。

4.3.1 基于委托的车辆追踪器

下面构造一个委托给线程安全类的车辆追踪器。我们将车辆的位置保存到一个Map对象中,因此首先要实现一个线程安全的Map类,ConcurrentHashMap(上面的例子中使用的是HashMap)。我们还可以用一个不可变的Point类来代替MutbalePoint来保存位置:

//            4-6  在DelegatingVehicleTracker中使用的不可变Point类
@Immutable
public class Point {
    public final int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

由于Point是不可变的,因而它是线程安全的。不可变的值可以被自由地共享与发布,因此在返回location时不需要赋值。

在DelegatingVehicleTracker中,所有对状态的访问都由ConcurrentHashMap来管理,Map所有的键值对都是不可变的

//      4-7  将线程安全委托给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);  //所有对状态的访问都由ConcurrentHashMap来管理
        unmodifiableMap = Collections.unmodifiableMap(locations);  //Map所有的键值对都是不可变的
    }

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

    // 返回locations的静态拷贝而非实时拷贝
    public Map<String, Point> getLocationsAsStatic() {
        return Collections.unmodifiableMap(
                new HashMap<String, Point>(locations));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

如果使用MutablePoint类就会破坏封闭性,因为getLocations会发布一个指向可变状态的引用。

在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而在使用委托的车辆追踪器中返回的是一个不可修改但却实时的车辆位置视图。这意味着,如果线程A调用getLocations,而线程B随后修改了某些点的位置,那么在返回给线程A的Map中将反映出这些变化。

如果需要一个不发生变化的车辆视图(快照),那么getLocations可以返回对locations这个Map对象的一个浅拷贝(Shallow Copy)。由于Map的内容是不可变的,因此只需赋值Map的结构,而不用复制它的内容(getLocations并不能确保返回一个线程安全的Map)

//          4-8    返回locations的竞态拷贝而非实时拷贝
 public Map<String, Point> getLocationsAsStatic() {
        return Collections.unmodifiableMap(
                new HashMap<String, Point>(locations));
    }
  • 1
  • 2
  • 3
  • 4
  • 5

4.3.2 独立的状态变量

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

在VisualComponent 中,允许客户程序注册监控鼠标和键盘等事件的监听器。鼠标事件监听器和键盘事件监听器之间不存在任何关联,二者是彼此独立的。

因此VisualComponent 可以将线程安全性委托给两个线程安全的监听器列表

//             4-9   将线程安全性委托给多个状态变量
public class VisualComponent {
    private final List<KeyListener> keyListeners
            = new CopyOnWriteArrayList<KeyListener>(); //CopyOnWriteArrayList是线程安全的链表
    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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

4.3.3 当委托失效(When Delegation Fails)

大多数组合对象都不会像VisualComponent这样简单,在它们的状态变量之间存在着某些不变性条件。

NumberRange使用了两个AtomicInteger来管理状态,并且包含一个约束条件,即下界要小于等于上界

//      4-10   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 IllegalArgumentException("can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // 注意——不安全的“先检查后执行”
        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());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

NumberRange不是线程安全的,没有维持对下界和上界进行约束的不变性条件。因为setLower和setUpper都是“先检查后执行”的操作,但它们没有使用足够的加锁机制来保证这些操作的原子性,可能出现(5,4)的取值范围。

因此,虽然AtomicInteger是线程安全的,但经过组合得到的类却不是。由于状态变量lower和upper不是彼此独立的,因此 NumberRange不能将线程安全性委托给它的线程安全 状态变量。

NumberRange可以通过加锁机制来维护不变性条件来确保其线程安全性,如用一个锁来保护lower和upper。此外,还可以避免发布lower和upper,从而防止客户代码破坏其不变性条件。

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

4.3.4 发布底层的状态变量(Publishing Underlying State Variables)

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

例如,发布VisualComponent中的mouseListener和keyListener等变量就是安全的。由于VisualComponent并没有在监听器链表的合法状态上施加任何约束,因此这些与可以声明为共有域或者发布,而不会破坏线程安全性。

4.3.5 发布状态的车辆追踪器

通过使用SafePoint,可以构造一个发布其底层可变状态的车辆追踪器,还能确保其线程安全性不被破坏。

//            4-11  线程安全且可变的Point类
@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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

PublishingVehicleTracker将线程安全性委托给底层的 ConcurrentHashMap,Map中的元素是线程安全且可变的Point。 getLocations()方法返回底层Map的一个不可变副本,调用者不能增删车辆,但可以通过修改返回Map的SafePoint的值来改变车辆的位置,

//            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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

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

Java类库中包含许多有用的“基础模块”类,通常,我们应该优先选择重用这些现有的类而不是创建新的类。

第一种策略:要添加一个新的原子操作,最安全的方法是修改原始的类,但通常无法做到,因为你可能无法访问或者修改类的源代码。要想修改原始的类,就需要理解代码中的同步策略,这样增加的功能才能与原有的设计保持一致。如果直接将新方法添加到类中,那么意味这实现同步策略的所有代码仍然处于一个源代码文件中,从而更容易理解与维护。

第二种策略:另一种方法是继承这个类。

BetterVector对Vector进行了扩展(继承了Vector),并添加了一个新方法putIfAbsent.

//      4-13  继承Vector并增加一个“若没有则添加”方法
@ThreadSafe
public class BetterVector <E> extends Vector<E> {
    // When extending a serializable class, you should redefine serialVersionUID
    //Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)
    static final long serialVersionUID = -3963416950630760754L;

    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !contains(x);
        if (absent)
            add(x);
        return absent;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

但并没所有类都像Vector一样将状态向子类公开,因此也就不适合这种方法。

继承方法比直接将代码添加到类中更加脆弱,因为现在的同步策略实现被分布到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏,因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。(Vector的规范中定义了它的同步策略,因此BetterVector不存在这个问题)

4.4.1 客户端加锁机制(Client-side Locking)

第三种策略:扩展类的功能,但并不是继承类本身,而是将扩展代码放入一个“辅助类”中。

//           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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上面的代码包含“若没有则添加”操作的辅助类,用于对线程安全的List执行操作,但这是错误的,并不能实现线程安全性。即使putIfAbsent被声明为synchronized类型的变量。

问题在于在错误的锁上进行了同步,无论List使用哪一个锁来保护它的状态,可以确定的是这个锁并不是 BadListHelper上的锁。 BadListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但却使用了同步的锁,这意味着putIfAbsent相对于List的其他操作来说并不是原子的。

要使该方法正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁。
客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。

包含“若没有则添加”操作的辅助类,用于对线程安全的List执行操作

//           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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中。
客户端加锁却更加脆弱,因为它将类C的加锁代码放到与C完全无关的其他类中,当在那些并不承诺遵守加锁策略的类上使用客户端加锁时,要特别小心。

客户端加锁机制与继承类机制有很多共同点,二者都是将派生类的行为与基类的实现耦合在一起。
继承会破坏实现的封装性,客户端加锁同样会破坏同步策略的封装性。

4.4.2 组合(Composition)

当为现有的类添加一个原子操作时,有一种更好的方法:组合(Composition)

ImprovedList通过将List对象委托给底层的的List实例来实现List的操作,同时还添加了一个原子的putIfAbsent方法。(ImprovedList假设将某个链表对象传给构造函数以后,客户代码不会再直接使用这个对象,而只能通过ImprovedList来访问它)

//            4-16  通过组合实现“若没有则添加”
@ThreadSafe
public class ImprovedList<T> implements List<T> {       //实现List接口
    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) {//Java监视器模式
        boolean contains = list.contains(x);
        if (contains)
            list.add(x);
        return !contains;
    }
    public synchronized void clear() { list.clear(); }
    //按照类似方式委托list的其他方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

ImprovedList通过自身的内置锁增加了一层额外的加锁,它并不关心底层的List是否是线程安全的。
事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。


4.5 将同步策略文档化(Documenting Synchronization Policies)

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

猜你喜欢

转载自blog.csdn.net/qq_40182703/article/details/80715242