好的,在这里我将尝试解释我使用 Java 语言解决问题的方法。
一切都将在 SumExpression 的示例中进行解释 - 用于将其他两个表达式的结果相加的表达式。
用户代码
我从最直接的方法开始——观察者模式。每个表达式都将侦听其依赖项以查找缓存失效。这是以这种方式实现的 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
and 表达式a
和之间的依赖关系列表b
。
核心代码
接下来,我EvaluationContext
由全局缓存类提供,它将缓存数据存储在类似于 的结构中WeakHashMap<Expression, Object>
,并将依赖图数据存储在 DAG 中,节点类型为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)而失败。所以,我不知道该怎么办。请帮忙。