Java中的线程安全映射

时间:2014-04-21 17:43:22

标签: java multithreading thread-safety

我理解多线程和同步的整体概念,但我是编写线程安全代码的新手。我目前有以下代码段:

synchronized(compiledStylesheets) {
    if(compiledStylesheets.containsKey(xslt)) {
        exec = compiledStylesheets.get(xslt);
    } else {
        exec = compile(s, imports);
        compiledStylesheets.put(xslt, exec);
    }
}

其中compiledStylesheetsHashMap(私有,最终)。我有几个问题。

编译方法可能需要几百毫秒才能返回。这似乎很长一段时间来锁定对象,但我没有看到替代方案。此外,除了synchronized块之外,没有必要使用Collections.synchronizedMap,对吗?除了初始化/实例化之外,这是唯一能够访问此对象的代码。

或者,我知道ConcurrentHashMap的存在,但我不知道这是否过度。 putIfAbsent()方法在此实例中不可用,因为它不允许我跳过compile()方法调用。我也不知道它是否能解决containsKey()之后但put()之前修改的问题。问题,或者在这种情况下是否真的引起关注。

编辑:拼写

5 个答案:

答案 0 :(得分:3)

我认为您正在寻找Multiton

有一个非常好的Java here,@ innlas不久前发布了。{/ p>

答案 1 :(得分:2)

您可以在竞争条件下偶尔出现双重编译的样式表,从而放松锁定。

Object y;

// lock here if needed
y = map.get(x);
if(y == null) {
    y = compileNewY();

    // lock here if needed
    map.put(x, y); // this may happen twice, if put is t.s. one will be ignored
    y = map.get(x); // essential because other thread's y may have been put
}

这需要getput为原子,在ConcurrentHashMap的情况下也是如此,您可以通过将单个调用包装到get和{{1}来实现在你的班级锁定。 (正如我试图解释的那样,“如果需要,可以锁定”评论 - 重点是你只需要打包个别电话,而不是一个大锁。)

这是一个标准的线程安全模式,即使使用put(和putIfAbsent)也可以最小化编译两次的成本。有时候仍然可以接受两次编译,但即使价格昂贵也应该没问题。

顺便说一下,你可以解决这个问题。通常,上面的模式不像ConcurrentHashMap这样的重函数使用,而是使用轻量级构造函数compileNewY。例如这样做:

new Y()

此外:

  

或者,我知道ConcurrentHashMap的存在,但我不知道这是否过度。

鉴于您的代码严重锁定,ConcurrentHashMap几乎肯定要快得多,所以并不过分。 (并且更有可能没有bug。并发错误 无法修复。)

答案 2 :(得分:2)

对于此类任务,我强烈推荐Guava caching support.

如果你不能使用该库,这里是Multiton.的紧凑实现FutureTask的使用来自assylias, here,来自OldCurmudgeon.的提示

public abstract class Cache<K, V>
{

  private final ConcurrentMap<K, Future<V>> cache = new ConcurrentHashMap<>();

  public final V get(K key)
    throws InterruptedException, ExecutionException
  {
    Future<V> ref = cache.get(key);
    if (ref == null) {
      FutureTask<V> task = new FutureTask<>(new Factory(key));
      ref = cache.putIfAbsent(key, task);
      if (ref == null) {
        task.run();
        ref = task;
      }
    }
    return ref.get();
  }

  protected abstract V create(K key)
    throws Exception;

  private final class Factory
    implements Callable<V>
  {

    private final K key;

    Factory(K key)
    {
      this.key = key;
    }

    @Override
    public V call()
      throws Exception
    {
      return create(key);
    }

  }

}

答案 3 :(得分:1)

请参阅下面的Erickson comment。使用带有Hashmaps的双重检查锁定不是很聪明

  

编译方法可能需要几百毫秒才能返回。这似乎很长一段时间来锁定对象,但我没有看到替代方案。

您可以使用双重检查锁定,并注意您在get之前不需要任何锁定,因为您从未从地图中删除任何内容。

if(compiledStylesheets.containsKey(xslt)) {
    exec = compiledStylesheets.get(xslt);
} else {
    synchronized(compiledStylesheets) {
        if(compiledStylesheets.containsKey(xslt)) {
            // another thread might have created it while
            // this thread was waiting for lock
            exec = compiledStylesheets.get(xslt);
        } else {
            exec = compile(s, imports);
            compiledStylesheets.put(xslt, exec);
        }
    }
}

}

  

另外,除了synchronized块之外,没有必要使用Collections.synchronizedMap,对吗?

正确

  

除了初始化/实例化之外,这是唯一能够访问此对象的代码。

答案 4 :(得分:0)

首先,您发布的代码为race-condition - 免费,因为containsKey()结果在compile()方法运行时永远不会更改。

如上所述,

Collections.synchronizedMap()对你的情况毫无用处,因为它使用synchronized作为互斥锁或你提供的另一个对象将所有地图方法包装到this块中(对于双参数)版本)。

使用ConcurrentHashMap的IMO也不是一个选项,因为它根据键hashCode()结果对锁进行条带化;它的并发迭代器在这里也没用。

如果你真的希望compile()阻止synchronized阻止,你可以在检查containsKey()之前预先计算。这可能会提取整体性能,但可能比在synchronized块中调用它更好。为了做出决定,我个人会考虑关键“未命中”的发生频率,因此,哪种选择是可取的 - 保持锁定时间更长或总是计算你的东西。