How to make a Arraylist thread-safe and serializeable using XMLEncoder

1.3k Views Asked by At

First off: I know there are many related posts in SO about this but one that I could find was able to help in my case.

So, what I'm doing is I got a very simple parent object that may have multiple child objects. Both objects comply to the java beans specifications (no-args constructor, setter and getter for all variables).

There may be multiple parents, which are saved in a repository class like this:

private final Map<String, Parent> parentItems = new ConcurrentHashMap<String, Parent>();

Every time a new parent is created, a repository class saves the parent list which is build from the parentItems list like this:

public List<Parent> getParents() {
    return new ArrayList<Parent>(parentItems.values());
}

The save operation is done by saving the List into an xml file using XMLEncoder. It looks like this:

public void saveParents(OutputStream os) {

    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    XMLEncoder encoder = null;
    try {
        Thread.currentThread().setContextClassLoader(Parent.class.getClassLoader());

        encoder = new XMLEncoder(os);
        encoder.writeObject(getParents());
        encoder.flush();
    } finally {
        if (encoder != null) {
            encoder.close();
        }
        Thread.currentThread().setContextClassLoader(cl);
    }
}

This all works fine. Now I want to add some children to the parent object and just add new child objects to the child List and rerun saveParents(). Here is where my trouble begins:

If I use a ArrayList within the parent to save the children, like this:

private ArrayList<Child> children = new ArrayList<Child>();

Everything works. The objects will be saved within the parent in the xml file. It looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.7.0_45" class="java.beans.XMLDecoder">
 <object class="java.util.ArrayList">
  <void method="add">
   <object class="org.mypackage.Parent" id="Parent0">
    <void property="children">
     <void method="add">
      <object class="org.mypackage.Child">
       <void property="displayName">
        <string>Child1</string>
       </void>
       <void property="id">
        <string>myid1</string>
       </void>
       <void property="parent">
        <object idref="Parent0"/>
       </void>
      </object>
     </void>
     <void method="add">
      <object class="org.mypackage.Child">
       <void property="displayName">
        <string>Child2</string>
       </void>
       <void property="id">
        <string>myid2</string>
       </void>
       <void property="parent">
        <object idref="Parent0"/>
       </void>
      </object>
     </void>
    </void>
    <void property="displayName">
     <string>Parent1</string>
    </void>
    <void property="id">
     <string>myid</string>
    </void>
   </object>
  </void>
 </object>
</java>

However: we know that ArrayList is not thread-safe and I know that the parent object may be altered by multiple people. So how to fix that? Use a synchronizedList(...) - right? Something like ...

private List<Child> children = Collections.synchronizedList(new ArrayList<Child>()); 

... then just quickly change the getter and setter to List instead of ArrayList and we should be fine. Okay that should work - but guess what: it fails :-(

Every time I add a child object (and run saveParent() after that) I get a StackOverflowException. If I look up the XML I see something like this:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.7.0_45" class="java.beans.XMLDecoder">
<void class="java.util.ArrayList">
 <void method="add">
  <object class="org.mypackage.Child" id="Child0">
   <void property="displayName">
    <string>Child1</string>
   </void>
   <void property="id">
    <string>myid</string>
   </void>
   <void property="parent">
    <object class="org.mypackage.Parent"/>
   </void>
  </object>
 </void>
</void>
<void class="java.util.ArrayList">
 <void method="add">
  <object idref="Child0"/>
 </void>
</void>
<void class="java.util.ArrayList">
 <void method="add">
  <object idref="Child0"/>
 </void>
</void>
<void class="java.util.ArrayList">
 <void method="add">
  <object idref="Child0"/>
 </void>
</void>
...... AND THIS GOES ON AND ON AND ON .......

Well, guess I know where that stack overflow came from... but why? It should be serializeable but well, look at it... How do I get a serializable, thread-safe List (or whatever other datatype that would fit) that doesn't blow up? Maybe any workaround you could think about in order to make that ArrayList (that works in this scenario) thread-safe? maybe by making the whole parent object thread-safe? (however: this may have other side-effects that's why simply getting that synchronizedList(...) to work would be the most elegant way)

1

There are 1 best solutions below

0
On

I created the following minimal complete verifiable example to replicate your problem:

public class Test {

    public static void main(String[] args) {
        List<Parent> parents = new ArrayList<>();
        // parents = Collections.synchronizedList(parents);

        Parent p1 = new Parent();
        Child c1 = new Child();
        Child c2 = new Child();
        p1.getChildren().add(c1);
        c1.setParent(p1);
        p1.getChildren().add(c2);
        c2.setParent(p1);

        parents.add(p1);

        try (XMLEncoder encoder = new XMLEncoder(System.out)) {
            encoder.writeObject(parents);
        }
    }

    public static class Parent {
        private List<Child> children = new ArrayList<>();

        public List<Child> getChildren() { return children; }
        public void setChildren(List<Child> children) { this.children = children; }
    }

    public static class Child {
        private Parent parent;

        public Parent getParent() { return parent; }
        public void setParent(Parent p) { parent = p; }
    }
}

The above non-synchronized collection code works, and produces your desired XML encoding. Uncommenting the parents = Collections.synchronizedList(parents); line, and the StackOverflowException results. Now to fix it.

The problem seems to stem from the parents list being serialized as if was an ArrayList<>, which it is not. It is actually a non-public java.util.Collections$SynchronizedRandomAccessList. If the StackOverflowException did not occur, your question would have been "I serialized a synchronizedList-wrapped-ArrayList, but is deserializes as just an ArrayList, which is wrong". You want to properly encode the synchronized list. For this, you need a PersistenceDelegate.

public class SynchronizedArrayListPD extends DefaultPersistenceDelegate {
    @Override
    protected Expression instantiate(Object oldInstance, Encoder out) {
        return new Expression(oldInstance, Collections.class, "synchronizedList",
                new Object[] { new ArrayList<>() });
    }

    @Override
    protected void initialize(Class<?> type, Object oldInstance, Object newInstance,
            Encoder out) {
        super.initialize(type, oldInstance, newInstance, out);

        List<?> list = (List<?>) oldInstance;
        for (Object item : list) {
            out.writeStatement(new Statement(oldInstance, "add", new Object[] { item }));
        }
    }
}

This delegate will create instructions to deserialize the list by passing to the Collections.synchronizedList() method an ArrayList<>, and then adding members to that resulting List<> object. Simply install the delegate after creating the XMLEncoder.

try (XMLEncoder encoder = new XMLEncoder(System.out)) {
    encoder.setPersistenceDelegate(parents.getClass(), new SynchronizedArrayListPD());
    encoder.writeObject(parents);
}

The #setPersistanceDelegate(cls, delegate) needs a Class<?> object. Normally, we'd use something like SynchronizedArrayList.class, but the actual class is not a public class, so we're grabbing the actual class from the object itself. This isn't type-safe; if you changed parents to be (say) a synchronized linked list, the SynchronizedArrayListPD would be used to synchronize that, which is wrong. Add appropriate cautions.

The resulting XML encoding:

<java version="1.8.0_101" class="java.beans.XMLDecoder">  
<object class="java.util.Collections" method="synchronizedList">  
  <object class="java.util.ArrayList"/>  
  <void method="add">  
   <object class="xml.Test$Parent" id="Test$Parent0">  
    <void property="children">  
     <void method="add">  
      <object class="xml.Test$Child">  
       <void property="parent">  
        <object idref="Test$Parent0"/>  
       </void>  
      </object>  
     </void>  
     <void method="add">  
      <object class="xml.Test$Child">  
       <void property="parent">  
        <object idref="Test$Parent0"/>  
       </void>  
      </object>  
     </void>  
    </void>  
   </object>  
  </void>  
 </object>  
</java>  

See the XML Encoder article for more info on creating Persistence Delegates.