如何在Java中优化并发操作?

时间:2012-11-25 21:33:34

标签: java multithreading

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

6 个答案:

答案 0 :(得分:3)

如果您正在从磁盘读取数据,则锁定争用不会成为您的性能问题。

您可以让两个线程获取整个缓存的锁定,执行读取,如果缺少值,释放锁定,从磁盘读取,获取锁定,然后如果值仍然缺失则写入,否则返回现在的值。

你将遇到的唯一问题是并发读取垃圾磁盘......但操作系统缓存会很热,因此磁盘不应该被过度删除。

如果这是一个问题,请将缓存切换为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();
}

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

喔。首先进行优化,使用ConcurrentHashMap替换地图并使用putIfAbsent,您将完全没有锁定! (但只有当你知道这是一个问题时才这样做)

答案 1 :(得分:3)

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

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

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

您应该使用现有的库,或者让它更简单。

答案 2 :(得分:1)

如果您的问题包含关键字“优化”,“并发”,并且您的解决方案包含一个复杂的锁定方案......您做错了。在这种风险投资中取得成功是可能的,但可能性很大。准备诊断奇怪的并发错误,包括但不限于死锁,活锁,缓存不一致...我可以在您的示例代码中发现多个不安全的做法。

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

您可以查看ConcurrentMap。您可能也喜欢CacheBuilder

答案 3 :(得分:1)

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

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

答案 4 :(得分:1)

我会看看Google的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) - 地图会处理所有&#34;创建(如果尚未存在)&#34;为你安全的东西。

答案 5 :(得分:0)

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