ОК, здесь я попытаюсь объяснить свой подход к проблеме с использованием языка Java.
Все будет объяснено на примере SumExpression - выражения, используемого для сложения результатов двух других выражений вместе.
Код пользователя
Я начал с самого простого подхода - паттерна Observer.Каждое выражение будет слушать свои зависимости для аннулирования кэша.Вот версия SumExpression, реализованная следующим образом:
public class SumExpression implements Expression<Integer> {
private final Expression<Integer> a;
private final Expression<Integer> b;
Integer value;
private Listener invalidator = new Listener() {
@Override
public void changed() {
invalidate();
}
};
public SumExpression(SimpleVariable<Integer> a, SimpleVariable<Integer> b) {
this.a = a;
this.b = b;
a.listeners().addListener(invalidator);// don't forget to call it!
b.listeners().addListener(invalidator);
}
public Integer getValue()
{
validate();
return value;
}
private void validate() {
if(value == null)
value = evaluate;
}
private void evaluate() {
value = null;
}
public void dispose() { // USER, DON'T FORGET TO CALL IT!!!
a.removeListener(invalidator);
b.removeListener(invalidator);
}
ListenerCollection listeners = new ListenerCollection();
@Override
public void addListener(Listener l) {
listeners.addListener(l);
}
@Override
public void removeListener(Listener l) {
listeners.removeListener(l);
}
}
Однако существует множество мест, где это может пойти не так, и что-то столь же простое, как сложение двух чисел, должно быть намного проще.Итак, я отделил логику от кеширования следующим образом:
public class SumExpression implements Expression<Integer> {
private final Expression<Integer> a;
private final Expression<Integer> b;
public SumExpression(Expression<Integer> a, Expression<Integer> b)
{
this.a = a;
this.b = b;
}
public Integer evaluate(EvaluationContext context)
{
return context.getValue(a)+context.getValue(b);
}
}
Намного проще, а?Обратите внимание, что здесь ответственность EvaluationContext
имеет два аспекта: он извлекает значения из кэша и собирает список зависимостей между SumExpression
и выражениями a
и b
.
Код ядра
Затем я предоставил EvaluationContext
глобальным классом кэширования, который хранит кэшированные данные в структуре, аналогичной WeakHashMap<Expression, Object>
, и данные графа зависимостей в группе обеспечения доступности баз данных с узлами типа WeakReference<Expression>
.
Вот моя реализация eval и update :
public <T1> T1 eval(final Expression<T1> expression)
{
Weak weak = weaken(expression);
T1 result = (T1) cache.get(weak);
if(result == null) {
result = expression.evaluate(new EvaluationContext()
{
@Override
public <T2> T2 getValue(Expression<T2> dependency) {
registerDependency(expression, dependency);
return eval(dependency);
}
});
cache.put(weak, result);
}
return result;
}
public void update(Expression<?> ex) {
changed(weaken(ex));
}
public void changed(Weak weak) {
cache.remove(weak);
dependencies.removeOutgoingArcs(weak);
for(Weak dependant : new ArrayList<Weak>(dependencies.getIncoming(weak))) {
changed(dependant);
}
}
Когда мой менеджер кэша запрашивает объект, он сначала проверяет в кэше.Если в кеше нет значения, оно запрашивает выражение для оценки.Затем выражение просит менеджер кэша разрешить его зависимости, вызвав метод getValue ().Это создает дугу в графе зависимостей.Этот график позже используется для аннулирования кэша.
Когда выражение недействительно, исследуется граф зависимостей и все зависимые кэши становятся недействительными.
Очистка кеша и графа зависимостей выполняется каккак только сборщик мусора уведомляет нас (через ReferenceQueue) о смерти некоторых объектов выражений.
В основном все работает так, как должно.Однако есть несколько хитрых случаев.
Хитрые случаи
Первый случай - это зависание промежуточной зависимости.Предположим, у нас есть следующий класс:
class SumProxyExpression implements Expression<Integer> {
private final Expression<Integer> a;
private final Expression<Integer> b;
public SumProxyExpression(Expression<Integer> a, Expression<Integer> b) {
this.a = a;
this.b = b;
}
@Override
public Integer evaluate(EvaluationContext context) {
Expression<Integer> s = new SumExpression(a, b);
return context.getValue(s);
}
}
Если мы создадим экземпляр c=SumProxyExpression(a,b)
и изменим значение на a
позже, мы бы хотели, чтобы c
также изменил его значение.Однако, если промежуточный SumExpression
уже собран мусором, это может не произойти.Чтобы бороться с этим, я не удаляю узлы из графа зависимостей, если они не являются конечными узлами (имеют только входящие или только исходящие дуги).
Другой случай, который я не знаю, как решить, этоследующее:
class SelfReferencingExpression implements Expression<List<?>> {
class Result extends ArrayList<Integer> {
}
@Override
public List<?> evaluate(EvaluationContext resolver) {
return new Result();
}
}
Если я кеширую результат такого выражения, он никогда не будет собирать мусор, потому что я сохраняю жесткие ссылки на кэшированные значения (Result
), и у него есть ссылка на содержащийкласс (выражение), поэтому выражение всегда достижимо, но никогда не может быть использовано.
Это утечка памяти, и я понятия не имею, как ее устранить.Сказать пользователю никогда не иметь такую ссылку возможно, но очень опасно, поэтому я хотел бы найти лучшее решение.
Альтернативные решения
Я также думал о реализации его с наследованием от общегоКласс самокэшируемого выражения вместо хранения всего в глобальном кэше.Это решение решит последний контрольный пример (SelfReferencingExpression), но завершится неудачно с первым (SumProxyExpression).Итак, я не знаю, что делать.Пожалуйста, помогите.