2014-04-21 62 views
3

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

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

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

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

另外,我知道ConcurrentHashMap的存在,但我不知道这是否过度杀伤。 putIfAbsent()方法在这种情况下不可用,因为它不允许我跳过compile()方法调用。我也不知道它是否会解决“在containsKey()之后但在put()之前修改”的问题,或者如果在这种情况下甚至真的担心这个问题。

编辑:拼写

+0

如何让exec语句将密钥添加到队列中,并让某个进程每隔一段时间执行一次队列呢?通过这种方式保证“安全”并且不会添加相同的密钥多次 –

+0

什么是xlst?你可以锁定。您可以使用“锁定键”缓存方法。或者使用'ConcurrentHashMap'。 –

+1

@MarshallTigerus我假设操作系统需要从该方法返回exec。 – assylias

回答

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); 
    } 

    } 

} 
1

请参阅埃里克森的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); 
     } 
    } 
} 

}

此外,它是不必使用Collections.synchronizedMap除了同步块,是否正确?

正确

这是打比初始化/实例化等这个对象的唯一代码。

+0

我很犹豫要两次调用containsKey,但我认为它在HashMap上速度很快...... – Derek

+2

这很糟糕。您无法将线程安全的双重检查锁定方式扩展到“HashMap”。只要在可能异步修改的'HashMap'上调用'get()',当由于重新散列造成的修改变得非原子性或不按顺序可见时,就会引发错误。 – erickson

+0

将它调用两次比锁定它可能更好;这是一种很少写,常阅读的情况。 –

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的情况下,真实,你可以通过包装单个呼叫到getput在你的类锁实现。 (正如我试图解释“如果需要锁定在这里”的意见 - 重点是你只需要打包个人电话,没有一个大锁)。

这是一个标准线程安全模式,即使与ConcurrentHashMap(和putIfAbsent)一起使用也可以最大限度地减少两次编译的开销。有时候编译两次仍然是可以接受的,但即使代价昂贵,它也应该是可以的。

顺便说一句,你可以解决这个问题。通常上述模式不能用于像compileNewY这样的重载函数,而是使用轻量级构造函数new Y()。例如做到这一点:

class PrecompiledY { 
    public volatile Y y; 
    private final AtomicBoolean compiled = new AtomicBoolean(false); 
    public void compile() { 
     if(!compiled.getAndSet(true)) { 
      y = compile(); 
     } 
    } 
} 
// ... 
ConcurrentMap<X, PrecompiledY> myMap; // alternatively use proper locking 

py = map.get(x); 
if(py == null) { 
    py = new PrecompiledY(); // much cheaper than compiling 

    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 
    y.compile(); // object that didn't get inserted never gets compiled 
} 

另外:

另外,我知道的ConcurrentHashMap的存在,但我不知道这是矫枉过正。

鉴于您的代码严重锁定,ConcurrentHashMap几乎肯定会快得多,所以不会过度。 (而更可能是无缺陷。并发错误是乐趣修复。)

+0

我无法理解这一部分。你能否澄清一下,“如果需要的话在这里锁定”评论?何时需要锁?此外,这段代码是否不允许通过多线程进行编译?那会是资源浪费,不是? –

+0

@MisrableVariable只要'get'和'put'是线程安全的,这个算法就是线程安全的。它不需要任何锁定访问'map'。 – djechlin

+0

@MisrableVariable请参阅我的编辑以了解如何解决浪费编译的问题。 – djechlin

0

首先,代码为您发布它是不含race-condition因为containsKey()结果不会改变,而compile()方法运行。

Collections.synchronizedMap()是无用的,你的情况如上所述,因为它使用两种this作为一个互斥体,或者您提供的另一个对象(对于两个参数版本)将所有地图的方法为​​块。

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

如果您确实想从​​块中删除compile(),您可以在检查containsKey()之前预先计算。这可能会使整体性能回归,但可能比在​​区块中调用它更好。为了做出决定,我个人会考虑多少次关键“miss”发生,因此,哪个选项是可取的 - 保持更长时间的锁定或总是计算你的东西。

3

我认为你正在寻找一个Multiton

有一个非常好的Java一here @enterlas发布前一段时间。

+0

链接的答案以两种不同的方式使用ConcurrentHashMap,具体取决于是否正在使用JDK8。两者都显得非常干净,表现出色,并且无条件竞赛。 –

+0

是的,这是我提出的解决方案,但我使用了'CountDownLatch'而不是'FutureTask'。像这样的事情是要走的路。 – erickson

+0

@erickson - 我喜欢“FutureTask”的优雅。 – OldCurmudgeon