luk2302 luk2302 - 1 month ago 8
Java Question

Thread-safe serializable Collection with atomic replace

I am facing a problem in my program when multiple threads access the same server over RMI. The server contains a list as a cache and performs some expensive computation sometimes changing that list. After the computation finished the list will be serialized and sent to the client.

First Problem: if the list is changed while being serialized (e.g. by a different client requesting some data) a

ConcurrentModificationException
is (probably) thrown, resulting in a
EOFException
for the RMI call / the deserialization on the client-side.

Therefore I need a some kind of list-structure which is "stable" for serialization while possibly being changed by a different thread.

Solutions we tried:


  • regular ArrayList / Set - not working because of concurrency

  • deep-copying the entire structure before every serialization - faaar too expensive

  • CopyOnWriteArrayList
    - expensive as well since it copies the list and



revealing the Second Problem: we need to be able to atomically replace any element in the list which is currently not thread-safe (first delete, then add (which is even more expensive)) or only doable by locking the list and therefore only doing the different threads in sequence.

Therefore my question is:


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 :/


Illustration of the problem (C=compute, A=add to list, R=remove from list, S=serialize)

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

Answer Source

My wrong initial thought was that the CopyOnWriteArrayList was a bad idea since it copies everything. But of course it does only perform a shallow copy, copying only the references, not a deep copy copying all Objects as well.

Therefore we clearly went with the CopyOnWriteArrayList because it already offered a lot of the needed functionality. The only remaining problem was the replace which even got more complex to be a addIfAbsentOrReplace.

We tried the CopyOnWriteArraySet but that did not fit our need because it only offered addIfAbsent. But in our case we had a instance of a class C called c1 which we needed to store and then replace with a updated new instance c2. Of course we overwrite equals and hashCode. Now we had to choose wether or not we wanted the equality to return true or false for the two only minimally different objects. Both options did not work, because

  • true would mean that the objects are the same and the set would not even bother adding the new object c2 because c1 already is in
  • false would mean c2 would be added but c1 would not be removed

Therefore CopyOnWriteArrayList. That list already offers a

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

which somewhat fits our needs. It lets us replace the object we need via custom comparison.

We utilized it in the following way:

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