不在不同线程中重新评估昂贵的数据

Posted

技术标签:

【中文标题】不在不同线程中重新评估昂贵的数据【英文标题】:Not reevaluating expensive data in different threads 【发布时间】:2021-12-23 13:48:57 【问题描述】:

我有这样的方法

public Object doSomethingExpensive(String x);

现在,如果我处理了这个方法,我可以将结果保存在 HashMap 中,例如,它们的键是字符串 x,值是结果对象。

如果该地图中存在数据,我不必再次处理它。

但现在我几乎同时收到两个请求。 在这种情况下,我想让第二个请求等待,直到第一个请求完成,第二个请求也可以在计算后得到第一个请求的结果,所以我不必计算两次或并行两次。

关键是,我不能用

public synchronized Object doSomethingExpensive(String x);

因为如果 String x 是其他东西,Object 就是其他东西。 所以我需要在那个字符串 x 上进行一些同步。

但 synchronized(x) 是不可能的,因为 java 中的字符串文字......

另外,如果没有字符串而是对象作为 x,那么我可能会在第二个请求中获得与请求 1 相关的内容相同的类似对象,但它们每个都是其他一些对象。

是的,所以我的问题是,如何解决这个问题,如何防止并行计算 String x 其结果两次,如何同步它并将结果缓存在 HashMap 中。

【问题讨论】:

【参考方案1】:

不知道是否理解你的问题,如果是为了避免重复计算,这本好书(Java Concurrency in Practice)给出了一个解决方案的例子:

  private final Map<String, Future<Object>> cache
      = new ConcurrentHashMap<String, Future<Object>>();

  public Object doSomethingExpensive(String x) throws InterruptedException 
    while (true) 
      Future<Object> future = cache.get(x);
      if (future == null) 
        Callable<Object> callable = new Callable<Object>() 
          @Override
          public Object call() throws Exception 
            // doSomethingExpensive todo
            return new Object();
          
        ;
        FutureTask<Object> futureTask = new FutureTask<>(callable);
        future = cache.putIfAbsent(x, futureTask);
        if (future == null) 
          future = futureTask;
          futureTask.run();
        
      
      try 
        return future.get();
       catch (CancellationException e) 
        cache.remove(x);
       catch (ExecutionException e) 
        throw new RuntimeException(e.getCause());
      
    
  

编辑: 来自cmets,使用JAVA8#ConcurrentHashMap#computeIfAbsent,真的很方便:

    ConcurrentHashMap<String, Object> concurrentHashMap = new ConcurrentHashMap<>();

    public Object doSthEx(String key) 
        return concurrentHashMap.computeIfAbsent(key, new Function<String, Object>() 
            @Override
            public Object apply(String s) 
                // todo 
                return new Object();
            
        );
    

或者使用一些库来获得评论中提到的更全面的功能:https://github.com/ben-manes/caffeine。

【讨论】:

Java 8 的 ConcurrentHashMap#computeIfAbsent 方法原生支持这一点。一个缓存库,比如Caffeine,增加了对限制大小的支持。 "在计算过程中,其他线程对该映射的一些尝试更新操作可能会被阻塞,因此计算应该简短而简单。"在computeIfAbsent 上写在javadoc 中。因此,如果我使用它来防止两次为 String X 计算结果,我还可以防止计算此映射中的任何其他 String x2? @zysaaa 为什么将线程代码(这里作为 FutureTask)放入 doSomethingExpensive/computeIfAbsent 中?......因为有问题的阻塞还有地图中的其他操作,所以你做了同步块的时间很短? @RobinKreuzer 该文档是因为 ConcurrentHashMap 锁定了 hashbin,这意味着对不同条目的写入可能会延迟。像数据库加载这样的典型成本是可以的,但是非常慢的操作是不合适的。在这种情况下,您将使用未来,但通过插入调用者在映射操作之外运行它,以便您获得每个条目锁定并且不使用额外的线程。这些在 Caffeine 的 faq 中进行了讨论。 @RobinKreuzer 未来是一个方便的持有者对象,它包含一个内部锁。除了更好、内置并且具有众所周知的意图之外,它等同于自定义持有人。这两种解决方案(直接计算、未来)的不同之处在于使用每个条目锁定或锁条带化,后者以低冲突率共享锁(映射的初始容量决定锁的数量)。我认为没有更好的解决方案,只是编写等效代码的不同风格。

以上是关于不在不同线程中重新评估昂贵的数据的主要内容,如果未能解决你的问题,请参考以下文章

在python中恢复具有不同输入的进程或重新启动它是否更好?

Angular 7 - 重新加载/刷新数据不同的组件

输入不在 SimpleForm 内重新呈现

JAVA 线程池 其中一个线程执行失败 则线程重新执行或者重新提交任务 急

在节中重新加载标题而不在 swift 中重新加载节行

您可以在不同的线程上重新引发 .NET 异常吗?