【问题标题】:Java synchronization on Collection with expensive operations具有昂贵操作的 Collection 上的 Java 同步
【发布时间】:2011-10-26 17:19:58
【问题描述】:

我在我的函数doMapOperation 中有一个名为synchronizedMap 的同步列表。在这个函数中,我需要从地图中添加/删除项目并对这些对象执行昂贵的操作。我知道我不想在同步块中调用昂贵的操作,但我不知道在执行这些操作时如何确保地图处于一致状态。这样做的正确方法是什么?

这是我的初始布局,我确信这是错误的,因为您想避免在同步块中调用昂贵的操作:

public void doMapOperation(Object key1, Object key2) {
    synchronized (synchronizedMap) {

        // Remove key1 if it exists.
        if (synchronizedMap.containsKey(key1)) {
            Object value = synchronizedMap.get(key1);
            value.doExpensiveOperation(); // Shouldn't be in synchronized block.

            synchronizedMap.remove(key1);
        }

        // Add key2 if necessary.
        Object value = synchronizedMap.get(key2);
        if (value == null) {
            Object value = new Object();
            synchronizedMap.put(key2, value);
        }

        value.doOtherExpensiveOperation(); // Shouldn't be in synchronized block.
    } // End of synchronization.
}

我想作为这个问题的延续,您将如何循环执行此操作?

public void doMapOperation(Object... keys) {
    synchronized (synchronizedMap) {

        // Loop through keys and remove them.
        for (Object key : keys) {
            // Check if map has key, remove if key exists, add if key doesn't.
            if (synchronizedMap.containsKey(key)) {
                Object value = synchronizedMap.get(key);
                value.doExpensiveOperation(); // Shouldn't be here.

                synchronizedMap.remove(key);
            } else {
                Object value = new Object();
                value.doAnotherExpensiveOperation(); // Shouldn't here.

                synchronizedMap.put(key, value);
            }
        }
    } // End of synchronization block.
}

感谢您的帮助。

【问题讨论】:

  • 这里没有足够的信息来知道在缓慢的操作过程中是否可以放弃锁。关键问题是是否所有这些工作都必须以原子方式发生在其他线程上?如果不是,哪些操作应该是原子的?操作是否必须在整个地图上以原子方式发生?换句话说,如果你能确保对单个键的操作是原子的呢?

标签: java collections synchronization


【解决方案1】:

您可以像这样在同步块之外执行昂贵的操作:

public void doMapOperation(Object... keys) {
    ArrayList<Object> contained = new ArrayList<Object>();
    ArrayList<Object> missing = new ArrayList<Object>();

    synchronized (synchronizedMap) {
        if (synchronizedMap.containsKey(key)) {
            contained.add(synchronizedMap.get(key));
            synchronizedMap.remove(key);
        } else {
            missing.add(synchronizedMap.get(key));
            synchronizedMap.put(key, value);
        }
    }

    for (Object o : contained)
        o.doExpensiveOperation();
    for (Object o : missing)
        o.doAnotherExpensiveOperation();
}

唯一的缺点是您可能在将值从synchronizedMap 中删除后对其执行操作。

【讨论】:

    【解决方案2】:

    您可以为您的synchronizedMap 创建一个包装器,并确保containsKeyremoveput 等操作是同步方法。然后只会同步对地图的访问,而您的昂贵操作可以在同步块之外进行。

    另一个优点是将昂贵的操作保持在同步块之外,如果操作调用另一个同步映射方法,您可以避免可能的死锁风险。

    【讨论】:

    • 我知道包装器有助于在调用这些方法时使它们同步,但我还想确保在修改 sychronizedMap 的过程中地图不会在其他地方被修改。
    【解决方案3】:

    在第一个 sn-p 中:在 if 子句中声明这两个值,然后在 if 子句中分配它们。使 if 子句同步,并在外部调用昂贵的操作。

    在第二种情况下,做同样的事情,但在循环内。 (在循环内同步)。当然,您可以在循环外只使用一个synchronized 语句,并简单地填充一个List 对象以调用昂贵的操作。然后,在第二个循环中,在同步块之外,对列表中的所有值调用该操作。

    【讨论】:

    • 有什么反对在一个方法中拥有多个同步块的吗?在第二种情况下,如果您循环数百次,则您正在同步数百次。这很糟糕吗?
    • 如果你需要同步一百个操作,那么你需要一百个同步。请检查我的更新以获取替代方法
    【解决方案4】:

    我们应该忘记小的效率,比如大约 97% 的时间: 过早的优化是万恶之源。然而我们不应该通过 在关键的 3% 中增加我们的机会。一个好的程序员不会 被这样的推理所迷惑而自满,他将明智地看待 仔细检查关键代码;但只有在该代码已经 确定。 — 唐纳德·高德纳

    你只有一个方法,doMapOperation()。如果这种方法继续进行块同步,你的表现如何?如果您不知道,那么您如何知道何时获得了性能良好的解决方案?您是否准备好处理对昂贵操作的多次调用,即使它们已从地图中移除?

    我并不是要居高临下,因为也许您比您所传达的更了解手头的问题,但您似乎正在进入一个您可能没有准备好也可能没有准备好的优化级别有必要。

    【讨论】:

      【解决方案5】:

      实际上,只需一次同步命中即可完成所有操作。第一次删除可能是最简单的。如果您知道对象存在,并且知道 remove 是原子的,为什么不直接删除它,如果返回的不是 null 调用昂贵的操作?

       // Remove key1 if it exists.
              if (synchronizedMap.containsKey(key1)) {
                  Object value = synchronizedMap.remove(key1);
                  if(value != null){ //thread has exclusive access to value 
                    value.doExpensiveOperation();
                  }
              }
      

      对于 put,因为它很昂贵并且应该是原子的,所以你很不走运并且需要同步访问。我建议使用某种计算地图。看看 google-collections 和 MapMaker

      您可以创建一个 ConcurrentMap,例如根据您的密钥构建昂贵的对象

       ConcurrentMap<Key, ExpensiveObject> expensiveObjects = new MapMaker()
             .concurrencyLevel(32)
             .makeComputingMap(
                 new Function<Key, ExpensiveObject>() {
                   public ExpensiveObject apply(Key key) {
                     return createNewExpensiveObject(key);
                   }
                 });
      

      这只是memoization的一种形式

      在这两种情况下,您根本不需要使用synchronized(至少是明确的)

      【讨论】:

        【解决方案6】:

        如果Map 中没有空值,则根本不需要containsKey() 调用:您可以使用Map.remove() 来删除该项目并告诉您它是否存在。所以你的同步块的真实内容只需要这样:

        Object value = Map.remove(key);
        if (value != null)
          value.doExpensiveOperation();
        else
        {
          value = new Value();
          value.doExpensiveOperation();
          map.put(key,value);
        }
        

        如果昂贵的操作本身不需要同步,即如果您不介意 Map 的其他客户端在操作时看到该值,则可以进一步简化为:

        Object value = Map.remove(key);
        if (value == null)
        {
          value = new Value();
          map.put(key,value);
        }
        value.doExpensiveOperation();
        

        同步块可以在昂贵的操作之前终止。

        【讨论】:

          猜你喜欢
          • 2016-01-31
          • 1970-01-01
          • 2010-12-12
          • 1970-01-01
          • 2010-09-14
          • 2010-12-15
          • 2013-06-16
          • 2011-03-21
          • 1970-01-01
          相关资源
          最近更新 更多