0

我对 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;
}
4

6 回答 6

3

您的方案的复杂性已经讨论过了。这导致很难找到错误。例如,您不仅锁定非最终变量,甚至在将它们用作锁定的同步块的中间更改它们。多线程很难推理,这种代码几乎不可能:

    synchronized(objectLockRegistry) {
        if(objectLockRegistry.size() > objectLockRegistryMaxSize) {
            objectLockRegistry = new HashMap<>(); //brrrrrr...
        }
    }

特别是,2 次同时调用以获取特定字符串的锁定实际上可能会返回同一字符串的 2 个不同实例,每个实例都存储在您的 hashmap 的不同实例中(除非它们被实习),并且您不会锁定同一个显示器。

您应该使用现有的库或使其更简单。

于 2012-11-25T22:18:23.520 回答
3

如果您从磁盘读取,锁争用不会成为您的性能问题。

您可以让两个线程获取整个缓存的锁,进行读取,如果值丢失,释放锁,从磁盘读取,获取锁,然后如果值仍然丢失,则写入它,否则返回值现在就在那里。

您将遇到的唯一问题是并发读取会破坏磁盘...但是操作系统缓存会很热,因此不应过度破坏磁盘。

如果这是一个问题,那么将您的缓存切换为保存 aFuture<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();
}

是的,这是一个全局锁...但是您正在从磁盘读取数据,并且在证明性能瓶颈之前不要进行优化...

哦。第一次优化,把地图换成aConcurrentHashMap然后使用putIfAbsent,你就没有锁了!(但只有在您知道这是一个问题时才这样做)

于 2012-11-25T22:03:44.353 回答
1

如果您的问题包含关键字“优化”、“并发”,并且您的解决方案包含复杂的锁定方案……那么您做错了。在这种冒险中取得成功是有可能的,但几率对你不利。准备诊断奇怪的并发错误,包括但不限于死锁、活锁、缓存不一致……我可以在您的示例代码中发现多种不安全的做法。

在不成为并发之神的情况下创建安全有效的并发算法的唯一方法是采用其中一个预烘焙并发类并根据您的需要调整它们。除非您有非常令人信服的理由,否则这太难了。

你可以看看ConcurrentMap。您可能还喜欢CacheBuilder.

于 2012-11-25T21:44:38.877 回答
1

大多数关于多线程和并发的教程的开头都介绍了直接使用线程和同步。但是,许多实际示例需要更复杂的锁定和并发方案,如果您自己实现它们,它们会很麻烦且容易出错。为了防止重新发明轮子,创建了 Java 并发库。在那里,您可以找到许多对您有很大帮助的课程。尝试在谷歌上搜索有关 Java 并发和锁的教程。

作为可能对您有所帮助的锁的示例,请参阅http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReadWriteLock.html

于 2012-11-25T21:47:14.187 回答
1

而不是滚动你自己的缓存,我会看看谷歌的MapMaker。像这样的东西会给你一个锁缓存,当它们被垃圾收集时,它会自动使未使用的条目过期:

ConcurrentMap<String,String> objectLockRegistry = new MapMaker()
    .softValues()
    .makeComputingMap(new Function<String,String> {
      public String apply(String s) {
        return new String(s);
      });

有了这个,整个getObjectLock实现很简单return objectLockRegistry.get(objectId)- 地图以安全的方式为您处理所有“如果不存在则创建”的东西。

于 2012-11-25T22:14:19.577 回答
0

我会对你做类似的事情:只需创建一个对象映射(新对象())。
但与你不同的是,我会使用TreeMap<String, Object> 或 HashMap 你称之为 lockMap。每个文件锁定一个条目。lockMap 对所有参与的线程都是公开的。
每次对特定文件的读取和写入,都会从映射中获取锁。并在该锁定对象上使用 syncrobize(lock) 。
如果 lockMap 不固定,并且它的内容 chan 发生变化,那么对 map 的读写也必须同步。(syncronized (this.lockMap) {....})
但是您的 getObjectLock() 不安全,请将所有内容与您的锁同步。(双重检查锁定在 Java 中不是线程安全的!)推荐的书:Doug Lea,Java 中的并发编程

于 2012-11-25T21:43:39.627 回答