1934 words
10 minutes
Java并发编程-对象的组合
NOTE

本篇笔记基于《Java并发编程实战》第4章 - 对象的组合

1. 设计线程安全的类#

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

  • 找出构成对象状态的所有变量

    • 如果对象中所有的域都是基本类型的变量,那么这些域构成对象的全部状态

      // 在这个类中Counter只有一个域value
      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;
          }
      }  
      
    • 如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。例如LinkedList 的状态就包括链表中所有节点对象的状态。

  • 找出约束状态变量的不变性条件

    • 例如在前文给出的Counter 中,其状态空间为Long.MIN_VALUELong.MAX_VALUE ,但同时由于它是计数器,因此还存在着一个限制,即不能是负值
    • 后验条件:例如Counter 当前状态为17,那么下一个有效状态只能为18
    • 先验条件:例如不能从空队列中移除一个元素
    • 类中可以同时包含约束多个状态变量的不变性条件,但是相关变量必须在单个 原子操作中进行读取或更新,而不能通过多次原子操作
  • 建立对象状态的并发访问管理策略

    • 为了防止多个线程在并发访问同一个变量时产生干扰,这些对象要么应该是线程安全的对象,要么是事实不可变的对象,或是由锁来保护的对象

2. 实例封闭#

如果一个对象不是线程安全的,那么可以通过多种方式使其在多线程程序中安全使用,实例封闭就是一种有效的方式。通过将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

在下面这个例子中,虽然HashSet 并非线程安全的,但是由于mySet 是私有且不会逸出,唯一能访问mySet 的路径是addPersoncontainPerson 都有锁进行保护,因而PersonSet 是一个线程安全的类。但是在这个例子中并未对Person 的安全性做任何假设,因在要安全的使用Person 对象的可靠方法是使其成为一个线程安全的类。

// 通过封闭机制确保线程安全
public class PersonSet{
    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);
    }
}

由线程封闭原则及其推论可以得出Java监视器模式 。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。使用私有的锁对象而不是对象的内置锁(或任何其他可以通过公有方式访问的锁),可以使客户代码无法获得得到锁,从而避免一些其他问题。

下面就是一个监视器模式的例子:

// 通过一个私有锁来保护状态
public class PrivateLock{
    private final Object myLock = new Object();
    void someMethod(){
        synchronized (myLock){
            // 访问或修改widget的状态
        }
    }
}

3. 线程安全性的委托#

线程安全性委托 (Thread Safety Delegation)是一种设计模式或策略,它指的是通过将线程安全性责任委托给其他组件或对象来简化线程安全的管理,而不是在每个类中都实现线程安全的逻辑。通过这种方式,程序可以更加灵活地管理并发问题,同时避免每个类都重新实现繁琐的同步逻辑。

在下面这个例子中,就没有使用任何显示的同步,所有对状态的访问都交给了ConcurrentHashMap 来进行管理

TIP

ConcurrentMap 是 Java 中并发集合框架的一部分,它是 Map 接口的一个扩展,提供了线程安全的键值对存储结构,特别适用于多线程环境中对集合进行并发操作的场景。

// 不可变Point类
public class Point{
    private final int x,y;
    public Point(int x,int y){
        this.x = x;
        this.y = y;
    }
}
// 将线程安全委托给ConcurrentHashMap
public class DelegatingVehicleTracker{
    private final ConcurrentMap<String,Point> locations;
    private final Map<String,Point> unmodifiableMap;
    
    public DelegatingVehicleTracker(Map<String,Point> points){
        locations = new ConcurrentHashMap<>(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);
        }
    }
}

但是在大多数的组合对象中,情况往往不会这么简单,例如下面这个例子。在这个例子中NumberRange 使用了两个AtomicInteger 来管理状态,并且存在约束条件,即第一个值要小于第二个。虽然AtomicInteger 是线程安全的,但是由于状态变量彼此之间并不独立,因此它们的组合是不安全的,不能将线程安全性委托给它们。

WARNING

如果某个类含有复合操作,那么仅靠委托不足以实现线程安全性,类必须由额外的加锁机制以保障线程安全

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

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

目前Java的类库中已经存在大量的基础模块供我们使用,但大部分时候,现有的类并不能完全满足我们的需求,因此如何在不破坏线程安全性的情况下添加新的操作就成了一个重要问题。

假设现在需要一个线程安全的链表,它需要提供一个原子的“若没有则添加”的操作,我们通常有以下几种方法:

  1. 修改原始类: 这种方法是最安全的,但大多数时候我们可能无法访问到源码

  2. 扩展这个类: 这种方法会导致同步策略实现被分布到多个单独维护的源代码文件中,若底层改变了同步策略或是锁可能导致子类被破坏

    // 扩展Vector并增加一个“若没有则添加”的方法
    public class BetterVector<E> extends Vector<E> {
        public synchronized boolean putIfAbsent(E x) {
            boolean absent = !contains(x);
            if (absent)
                add(x);
            return absent;
        }
    }
    
  3. 客户端加锁: 这种实现方法是非常脆弱的,因此它将类C的加锁代码放到了与C完全无关的其他类中,若客户端不承遵循加锁机制则会出现问题

    // 通过客户端加锁来实现“若没有则添加”
    public class ListHelper<E> {
        public List<E> list = Collections.synchronizedList(new ArrayList<>());
    
        public synchronized boolean putIfAbsent(E x){
            boolean absent = !list.contains(x);
            if(absent){
                list.add(x);
            }
            return absent;
        }
    }
    
  4. 组合: 这种方式是相对最优的,因为它无需关心底层List 是否是线程安全或是会修改加锁机制,因为ImprovedList 会始终提供一致的加锁机制来保障线程安全性

    // 通过组合来实现“若没有则添加”
    public class ImprovedList<E> implements List<E> {
        private final List<E> list;
    
        public ImprovedList(List<E> list) {
            this.list = list;
        }
    
        public synchronized boolean putIfAbsent(E x) {
            boolean contains = list.contains(x);
            if (contains) {
                list.add(x);
            }
            return !contains;
        }
        
        public synchronized void clear() {
            list.clear();
        }
        // ... 按照类似的方法委托给list的其他方法
    }
    
Java并发编程-对象的组合
https://mj3622.github.io/posts/学习笔记/java并发编程/对象的组合/
Author
Minjer
Published at
2024-11-01