【问题标题】: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 中。

【问题讨论】:

    标签: java multithreading caching hashmap synchronized


    【解决方案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 未来是一个方便的持有者对象,它包含一个内部锁。除了更好、内置并且具有众所周知的意图之外,它等同于自定义持有人。这两种解决方案(直接计算、未来)的不同之处在于使用每个条目锁定或锁条带化,后者以低冲突率共享锁(映射的初始容量决定锁的数量)。我认为没有更好的解决方案,只是编写等效代码的不同风格。
    猜你喜欢
    • 1970-01-01
    • 2011-12-06
    • 1970-01-01
    • 1970-01-01
    • 2011-08-03
    • 2015-02-28
    • 2011-05-05
    • 2017-12-08
    • 1970-01-01
    相关资源
    最近更新 更多