Как сделать Arraylist потокобезопасным и сериализуемым с помощью XMLEncoder

#java #multithreading #serialization #arraylist #xmlencoder

#java #многопоточность #сериализация #arraylist #xmlencoder

Вопрос:

Во-первых: я знаю, что в SO есть много связанных сообщений об этом, но один, который я смог найти, смог помочь в моем случае.

Итак, что я делаю, я получил очень простой родительский объект, который может иметь несколько дочерних объектов. Оба объекта соответствуют спецификациям Java beans (конструктор без аргументов, установщик и получатель для всех переменных).

Может быть несколько родительских элементов, которые сохраняются в классе репозитория, подобном этому:

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

Каждый раз, когда создается новый родительский элемент, класс репозитория сохраняет родительский список, который создается из parentItems списка следующим образом:

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

Операция сохранения выполняется путем сохранения List в XML-файл с помощью XMLEncoder . Это выглядит так:

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

Все это работает нормально. Теперь я хочу добавить несколько дочерних объектов к родительскому объекту и просто добавить новые дочерние объекты в список дочерних объектов и перезапустить saveParents() . Вот где начинается моя проблема:

Если я использую a ArrayList внутри родительского элемента для сохранения дочерних элементов, например:

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

Все работает. Объекты будут сохранены в родительском файле в xml. Это выглядит примерно так:

 <?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>
  

Однако: мы знаем, что ArrayList это не потокобезопасно, и я знаю, что родительский объект может быть изменен несколькими людьми. Итак, как это исправить? Используйте synchronizedList(...) — правильно? Что-то вроде …

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

… затем просто быстро измените получатель и установщик на List вместо ArrayList , и все должно быть в порядке. Хорошо, это должно сработать, но угадайте, что: это не удается:-(

Каждый раз, когда я добавляю дочерний объект (и запускаю saveParent() после этого) Я получаю StackOverflowException . Если я посмотрю XML, я увижу что-то вроде этого:

 <?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 .......
  

Ну, думаю, я знаю, откуда взялось это переполнение стека … но почему? Он должен быть сериализуемым, но хорошо, посмотрите на него…
Как мне получить сериализуемый, потокобезопасный List (или любой другой подходящий тип данных), который не взорвется?
Может быть, какое-либо обходное решение, о котором вы могли бы подумать, чтобы сделать это ArrayList (которое работает в этом сценарии) потокобезопасным? может быть, сделав весь родительский объект потокобезопасным? (однако: это может иметь другие побочные эффекты, поэтому просто заставить это synchronizedList(...) работать было бы самым элегантным способом)

Комментарии:

1. На всякий случай, если кто-нибудь столкнется с подобной проблемой. Я не смог заставить работать другой тип данных, и у меня не было времени, чтобы сузить причину этого. должна быть одна из задействованных фреймворков, но они с закрытым исходным кодом, так что не повезло. однако: я реализовал блокировку по всему экземпляру для всех методов, которые будут вносить изменения в arraylist с использованием syncronized. Даже если это не будет хорошо масштабироваться, для моего текущего проекта этого достаточно, пока я не найду несколько альтернатив с открытым исходным кодом для фреймворков, которые выдают ошибку.

Ответ №1:

Я создал следующий минимально полный проверяемый пример, чтобы воспроизвести вашу проблему:

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

Приведенный выше несинхронизированный код коллекции работает и создает желаемую кодировку XML. Раскомментируйте parents = Collections.synchronizedList(parents); строку и StackOverflowException результаты. Теперь, чтобы исправить это.

Проблема, похоже, связана с parents тем, что список сериализуется так, как если бы он был an ArrayList<> , а это не так. На самом деле это непубличный java.util.Collections$SynchronizedRandomAccessList файл. Если StackOverflowException бы этого не произошло, ваш вопрос был бы «Я сериализовал a synchronizedList-wrapped-ArrayList , но десериализуется как просто an ArrayList , что неправильно». Вы хотите правильно закодировать синхронизированный список. Для этого вам понадобится 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 }));
        }
    }
}
  

Этот делегат создаст инструкции для десериализации списка путем передачи Collections.synchronizedList() методу an ArrayList<> , а затем добавления членов к этому результирующему List<> объекту. Просто установите делегат после создания XMLEncoder .

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

Для #setPersistanceDelegate(cls, delegate) этого нужен Class<?> объект. Обычно мы использовали бы что-то вроде SynchronizedArrayList.class , но фактический класс не является общедоступным классом, поэтому мы берем фактический класс из самого объекта. Это небезопасно для типов; если вы изменили parents его на (скажем) синхронизированный связанный список, SynchronizedArrayListPD он будет использоваться для синхронизации, что неверно. Добавьте соответствующие предостережения.

Результирующая кодировка XML:

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

Дополнительную информацию о создании делегатов сохраняемости см. В статье XML Encoder.