我对 Java 中的多线程仍然很犹豫。我在这里描述的内容是我的申请的核心,我需要正确对待这一点。该解决方案需要快速运行并且需要实际上安全。这行得通吗?欢迎任何建议/批评/替代解决方案。
<小时/>我的应用程序中使用的对象的生成成本较高,但很少更改,因此我将它们缓存在 *.temp 文件中。一个线程可能尝试从缓存中检索给定的对象,而另一个线程则尝试在缓存中更新该对象。检索和存储的缓存操作封装在 CacheService 实现中。
考虑这种情况:
Thread 1: retrieve cache for objectId "page_1".
Thread 2: update cache for objectId "page_1".
Thread 3: retrieve cache for objectId "page_2".
Thread 4: retrieve cache for objectId "page_3".
Thread 5: retrieve cache for objectId "page_4".
注意:线程 1 似乎检索了一个过时的对象,因为线程 2 拥有该对象的较新副本。这完全没问题,所以我不需要任何逻辑来赋予线程 2 优先级。
如果我在服务上同步检索/存储方法,那么我会不必要地减慢线程 3、4 和 5 的速度。多个检索操作在任何给定时间都将有效,但很少会调用更新操作。这就是为什么我想避免方法同步。
我认为我需要同步线程 1 和 2 专用的对象,这意味着锁定对象注册表。在这里,一个明显的选择是 Hashtable,但同样,Hashtable 上的操作是同步的,所以我正在尝试 HashMap。该映射存储一个字符串对象,用作同步的锁定对象,键/值将是正在缓存的对象的 id。因此,对于对象“page_1”,键将是“page_1”,锁定对象将是值为“page_1”的字符串。
如果我的注册表正确,那么我还想保护它不被太多条目淹没。我们不详细讨论原因。我们假设,如果注册表增长超过了定义的限制,则需要使用 0 个元素重新初始化。对于不同步的 HashMap 来说这是有一点风险的,但是这种洪水将超出正常的应用程序操作范围。这应该是非常罕见的情况,希望永远不会发生。但既然有可能,我就想保护自己免受它的侵害。
@Service
public class CacheServiceImpl implements CacheService {
private static ConcurrentHashMap<String, String> objectLockRegistry=new ConcurrentHashMap<>();
public Object getObject(String objectId) {
String objectLock=getObjectLock(objectId);
if(objectLock!=null) {
synchronized(objectLock) {
// read object from objectInputStream
}
}
public boolean storeObject(String objectId, Object object) {
String objectLock=getObjectLock(objectId);
synchronized(objectLock) {
// write object to objectOutputStream
}
}
private String getObjectLock(String objectId) {
int objectLockRegistryMaxSize=100_000;
// reinitialize registry if necessary
if(objectLockRegistry.size()>objectLockRegistryMaxSize) {
// hoping to never reach this point but it is not impossible to get here
synchronized(objectLockRegistry) {
if(objectLockRegistry.size()>objectLockRegistryMaxSize) {
objectLockRegistry.clear();
}
}
}
// add lock to registry if necessary
objectLockRegistry.putIfAbsent(objectId, new String(objectId));
String objectLock=objectLockRegistry.get(objectId);
return objectLock;
}
最佳答案
如果您从磁盘读取数据,锁争用不会成为性能问题。
您可以让两个线程获取整个缓存的锁,进行读取,如果值丢失,则释放锁,从磁盘读取,获取锁,然后如果值仍然丢失,则写入它,否则返回现在存在的值。
您遇到的唯一问题是并发读取会破坏磁盘...但是操作系统缓存会很热,因此磁盘不应被过度破坏。
如果这是一个问题,请将您的缓存切换为保存 Future<V>
代替<V>
。
get 方法将变成这样:
public V get(K key) {
Future<V> future;
synchronized(this) {
future = backingCache.get(key);
if (future == null) {
future = executorService.submit(new LoadFromDisk(key));
backingCache.put(key, future);
}
}
return future.get();
}
是的,这是一个全局锁...但是您正在从磁盘读取,并且在经过证明的性能瓶颈之前不会进行优化...
哦。第一个优化,将 map 替换为ConcurrentHashMap
并使用putIfAbsent
而且你根本就没有锁! (但只有当您知道这是一个问题时才这样做)
关于java - Java中如何优化并发操作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13555723/