我理解多线程和同步的整体概念,但我是编写线程安全代码的新手。我目前有以下代码段:
synchronized(compiledStylesheets) {
if(compiledStylesheets.containsKey(xslt)) {
exec = compiledStylesheets.get(xslt);
} else {
exec = compile(s, imports);
compiledStylesheets.put(xslt, exec);
}
}
其中compiledStylesheets
是HashMap
(私有,最终)。我有几个问题。
编译方法可能需要几百毫秒才能返回。这似乎很长一段时间来锁定对象,但我没有看到替代方案。此外,除了synchronized
块之外,没有必要使用Collections.synchronizedMap,对吗?除了初始化/实例化之外,这是唯一能够访问此对象的代码。
或者,我知道ConcurrentHashMap
的存在,但我不知道这是否过度。 putIfAbsent()
方法在此实例中不可用,因为它不允许我跳过compile()
方法调用。我也不知道它是否能解决containsKey()
之后但put()
之前修改的问题。问题,或者在这种情况下是否真的引起关注。
编辑:拼写
答案 0 :(得分:3)
答案 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
}
这需要get
和put
为原子,在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
块中调用它更好。为了做出决定,我个人会考虑关键“未命中”的发生频率,因此,哪种选择是可取的 - 保持锁定时间更长或总是计算你的东西。