javamultithreadingserializationcollectionscopyonwritearraylist

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:

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

Solution

  • 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

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