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