具有原子替换的线程安全可序列化集合

Thread-safe serializable Collection with atomic replace

当多个线程通过 RMI 访问同一个服务器时,我的程序遇到了问题。服务器包含一个列表作为缓存,并执行一些昂贵的计算,有时会更改该列表。计算完成后,列表将被序列化并发送给客户端。

第一个问题: 如果列表在序列化时被更改(例如,由不同的客户端请求一些数据)(可能)抛出 ConcurrentModificationException,导致EOFException 用于客户端的 RMI 调用/反序列化。

因此我需要某种列表结构,它对于序列化是“稳定的”,同时可能会被不同的线程更改。

我们尝试过的解决方案

揭示了第二个问题:我们需要能够自动替换列表中当前非线程安全的任何元素(先删除,然后添加(偶数更昂贵))或只能通过锁定列表来实现,因此只能按顺序执行不同的线程。

因此我的问题是:

Do you know of a Collection implementation which allows us to serialize the Collection thread-safe while other Threads modify it and which contains some way of atomically replacing elements?

A bonus would be if the list would not need to be copied before serialization! Creating a snapshot for every serialization would be okay, but still meh :/

问题说明(C=计算,A=添加到列表,R=从列表中删除,S=序列化)

Thread1 Thread2
      C 
      A
      A C
      C A
      S C
      S R <---- Remove and add have to be performed without Thread1 serializing 
      S A <---- anything in between (atomically) - and it has to be done without 
      S S       blocking other threads computations and serializations for long
        S       and not third thread must be allowed to start serializing in this
        S       in-between state
        S

最简单的解决方案是隐含到 ArrayList 的外部同步,可能通过这样的读写锁:

public class SyncList<T> implements Serializable {
    private static final long serialVersionUID = -6184959782243333803L;

    private List<T> list = new ArrayList<>();
    private transient Lock readLock, writeLock;

    public SyncList() {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        readLock = readWriteLock.readLock();
        writeLock = readWriteLock.writeLock();
    }

    public void add(T element) {
        writeLock.lock();
        try {
            list.add(element);
        } finally {
            writeLock.unlock();
        }
    }

    public T get(int index) {
        readLock.lock();
        try {
            return list.get(index);
        } finally {
            readLock.unlock();
        }
    }

    public String dump() {
        readLock.lock();
        try {
            return list.toString();
        } finally {
            readLock.unlock();
        }
    }

    public boolean replace(T old, T newElement) {
        writeLock.lock();
        try {
            int pos = list.indexOf(old);
            if (pos < 0)
                return false;
            list.set(pos, newElement);
            return true;
        } finally {
            writeLock.unlock();
        }
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        readLock.lock();
        try {
            out.writeObject(list);
        } finally {
            readLock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    private void readObject(ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        list = (List<T>) in.readObject();
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        readLock = readWriteLock.readLock();
        writeLock = readWriteLock.writeLock();
    }
}

提供您喜欢的任何操作,只需正确使用读锁或写锁即可。

我最初的错误想法是 CopyOnWriteArrayList 是个坏主意,因为它会复制所有内容。但是当然它只执行浅拷贝,只复制引用,而不是复制所有对象的深拷贝。

因此我们显然选择了 CopyOnWriteArrayList,因为它已经提供了很多需要的功能。唯一剩下的问题是 replace,它甚至变得更复杂成为 addIfAbsentOrReplace

我们尝试了 CopyOnWriteArraySet,但这不符合我们的需要,因为它只提供 addIfAbsent。但在我们的例子中,我们有一个名为 c1 的 class C 实例,我们需要存储它,然后用更新的新实例 c2 替换它。当然我们覆盖equalshashCode。现在我们必须选择我们是否希望两个仅有最小差异的对象等于 return truefalse。这两个选项都不起作用,因为

  • true 意味着对象是相同的,集合甚至不会费心添加新对象 c2 因为 c1 已经在
  • false 表示将添加 c2 但不会删除 c1

因此CopyOnWriteArrayList。该列表已经提供了

public void replaceAll(UnaryOperator<E> operator) { ... }

有点符合我们的需求。它允许我们通过自定义比较替换我们需要的对象。

我们通过以下方式使用它:

protected <T extends OurSpecialClass> void addIfAbsentOrReplace(T toAdd, List<T> elementList) {
    OurSpecialClassReplaceOperator<T> op = new OurSpecialClassReplaceOperator<>(toAdd);
    synchronized (elementList) {
        elementList.replaceAll(op);
        if (!op.isReplaced()) {
            elementList.add(toAdd);
        }
    }
}

private class OurSpecialClassReplaceOperator<T extends OurSpecialClass> implements UnaryOperator<T> {

    private boolean replaced = false;

    private T toAdd;

    public OurSpecialClassReplaceOperator(T toAdd) {
        this.toAdd = toAdd;
    }

    @Override
    public T apply(T toAdd) {
        if (this.toAdd.getID().equals(toAdd.getID())) {
            replaced = true;
            return this.toAdd;
        }

        return toAdd;
    }

    public boolean isReplaced() {
        return replaced;
    }
}