【问题标题】:How to achieve Map putIfAbsent semantics with computeIfAbsent efficiency?如何用computeIfAbsent效率实现Map putIfAbsent语义?
【发布时间】:2020-07-01 08:34:18
【问题描述】:

考虑以下代码:

ConcurrentHashMap<String, Value> map = new ConcurrentHashMap<>();

boolean foo(String key) {
    Value value = map.get(key);
    if (value == null) {
        value = map.putIfAbsent(key, new Value());
        if (value == null) {
            // do some stuff
            return true;
        }
    }
    // do some other stuff
    return false;
 }
    

假设foo() 被多个线程同时调用。还假设调用new Value() 很昂贵。代码很冗长,仍然会导致创建冗余的Value 对象。上述逻辑能否以保证不创建冗余Value 对象的方式实现(即最多调用一次new Value())?我正在寻找一个干净的实现——无需显式获取锁的最少代码。

computeIfAbsent 可能是一个不错的选择,但是它的返回语义不符合所需的逻辑。

【问题讨论】:

  • 不知道为什么你认为Value value = map.computeIfAbsent(key, Value::new); 不适用。
  • @assylias 好吧,值构造函数不带键。但是忽略参数的 lambda 并没有错。 computeIfAbsent(key, k -&gt; new Value())
  • @assylias 我指的是computeIfAbsentputIfAbsent 的返回语义。问题中的逻辑根据是否创建条目采用单独的代码路径。我们无法使用computeIfAbsent 清楚地解决这个问题
  • 听起来你有竞争条件。两个线程调用此方法,#1 创建并添加一个新对象,#2 获取新对象。如果 #2 先开始,它将表现得好像 #1 已经完成。根据您对我的其他评论的回复,也许您应该考虑使用“计算”​​,在这种情况下,您会获得旧值并且可以进行特殊分支。
  • @matt 不确定我是否理解竞态条件是如何发生的; compute 也不会公开是否创建了条目,因此需要此类输入的 compute 之外的代码(例如条件 return 语句)将不起作用。

标签: java


【解决方案1】:

完成这项工作的一些最小代码:

boolean foo(String key) {
    AtomicBoolean flag = new AtomicBoolean();
    Value value = map.computeIfAbsent(key, k -> {flag.set(true); return new Value();});
    if (flag.get()) {
        // do some stuff
    } else {
        // do some other stuff
    }
    return flag.get();
}

【讨论】:

  • 对,但是,JDK 提供了一个 putIfAbsent 来获取一个用于提供值的 lambda 会更干净:)
  • @DanielNitzan 是的,也不是。较小的接口更易于使用。一个包含 200 种方法的界面可能很全面,但这并不意味着它很容易使用,如果您必须花费数年时间寻找满足您需求的确切方法。仅仅因为你个人有一个不方便的极端情况并不意味着作者没有做出正确的决定。
  • @Michael 我一般不同意你的推理。但是,我觉得在这种特定情况下,作者应该通过提供它的 lambda 版本来增强 putIfAbsent(本身作为一种有用的 API 方法)。
  • @Michael 另外,我不会称其为极端情况,毕竟putIfAbsent 的存在是有充分理由的。根据是否创建条目采取不同的操作并非闻所未闻。
  • @DanielNitzan 我认为问题是,您想知道是否添加了新值,因为如果对象不存在,您会发生代码分支。不过,这正是 computeIfAbsent 的含义,如果该值不存在,则为代码分支。
【解决方案2】:

一种解决方案是在地图中存储Future&lt;Value&gt; 而不是Value

ConcurrentHashMap<String, Future<Value>> map = new ConcurrentHashMap<>();

boolean foo(String key) {
    Future<Value> value = map.get(key);
    if (value == null) {
        value = map.putIfAbsent(key, new FutureTask<Value>(() -> new Value()));
        if (value == null) {
            // do some stuff
            return true;
        }
    }
    // do some other stuff
    return false;
}

您可以通过调用value.get() 访问基础值,这将阻塞直到计算完成。

有可能会创建多个 FutureTask,但只有一个会到达地图,并且只会完成一次 new Value() 的计算。

【讨论】:

  • 问题在于,您将创建Value开始 推迟到实际需要的时候。如果调用foo,则程序等待5分钟,然后尝试检索该Value,调用者必须等待整个时间才能创建Value,此时它可能已在同步创建foo()(当前行为)或在 5 分钟不活动期间。您正在使用 Future 做一些事情,但是推送到执行程序而不是创建 FutureTask 会减轻调用者必须等待的某些情况
  • 你是对的,它甚至不适用于供应商,因为它每次都会重新计算。切换回 FutureTask。实际上,您可以在 putIfAbsent 之后通过调用 value.get() 来触发计算,或者您可以将其发送给执行程序。
【解决方案3】:

首先,让我们解决您不是原子行为的事实,并进行不必要的查找。两个线程可以同时通过第一个value == null 检查。现在不是真正的问题(除了将创建 2 个 Values,这很慢),但是如果有人在第二个 value == null 检查中添加 else 子句,则会等待发生错误。这样也更干净。

boolean foo(String key) {
    Value value = map.putIfAbsent(key, new Value());
    if (value == null) {
        // do some stuff
        return true;
    } 
    else {
       // do some other stuff
       return false;
    }
 }

现在让我们解决Value 创建速度慢的事实(听起来你在滥用构造函数,但无论如何)。

 boolean foo(String key) {
    final AtomicBoolean wasCreated = new AtomicBoolean(false);
    final Value value = map.computeIfAbsent(key, k -> {
        wasCreated.set(true);
        return new Value();
    });
    if (wasCreated.get()) {
        // do some stuff
        return true;
    } 
    else {
       // do some other stuff
       return false;
    }
 }

【讨论】:

  • 或者只是删除两个return 语句并在if else 之后添加return wasCreated.get();。您还可以删除 final 关键字 - 它们没有任何用处。并删除false。代码越少越好。
  • 我做了两次null 检查以避免在正常情况下创建不必要的对象,即密钥已经存在。此外,您的解决方案有效,但我们可以做得更好,也许通过 JDK 提供一个 putIfAbsent 来获取用于提供值的 lambda。
  • @DanielNitzan 一般来说,你应该非常小心地为并发映射组合检查和放置。这些比较和设置操作(putIfAbsentcomputeIfAbsent 等)是故意存在的,目的是使原子操作变得简单高效。当你写这篇文章时,它应该是一个危险信号,不仅仅是因为Value 创建速度很慢。未来需要注意的事项
【解决方案4】:

一种方法是使用本地状态并在computeIfAbsent的映射函数中更新它:

boolean foo(String key) {
    boolean[] b = { false };
    map.computeIfAbsent(key, k -> {
        // do some stuff
        b[0] = true;
        return new Value();
    });
    return b[0];
}

因为mappingFunction只有在key不存在的情况下才会运行,你可以保证重的new Value()只在必要的时候被调用并且返回值被设置为true只有当没有调用前的映射。

【讨论】:

    【解决方案5】:

    考虑您的方法foo(实际上是 putIfAbsent 的包装器)。

    String key = "test";
    if(foo(key)){
        //Positive Branch. 
    } else{
        //Negative Branch.
    }
    

    现在,在多线程环境中,线程 A 调用并完成 foo 并添加一个新值。它正在沿着积极的分支。在它进入正分支之前,它被安排在以后。线程 B 调用并完成 foo,它继续执行并进入 Negative 分支。

    Negative 分支在 Positive 分支之前执行,这在我看来是不想要的。在您的特定情况下,它可能没问题。 compute 可能是更好的替代品。

    map.compute( key, ( k, old ) -> {
        if(old==null){
            Value v = new Value();
            //positive branch.
            return v;
        } else{
            //negative branch.
            return old;
        }
    });
    

    现在您将自动判断该值是否存在。

    【讨论】:

    • 问题只与foo 内部的上下文有关,与外部代码调用foo 无关。如果你想基于foo的返回值在foo之外实现线程安全/原子执行,那么你需要正确同步你的代码。
    • @DanielNitzan 你似乎不理解我所描述的竞争条件,这与为什么你对computeIfAbsent 的期望有点极端有关。如果“问题仅与 foo 内部的上下文有关”,那么如果不存在就进行计算就足够了,因为它将消除不必要的价值创造并且没有显式锁定。
    猜你喜欢
    • 2018-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多