6

假设我有一个高读低写的并发映射,需要存储应用程序数据:

ConcurrentMap<UUID, Data> map = new ConcurrentHashMap<UUID, Data>();

然后,在启动期间并通过用户输入,将数据添加到地图中:

public void createData(Data newData) {
    map.put(newId, newData); // etc...
}

如果我需要更改数据,我应该:

A) 使 Data 类对象不可变,然后每次需要对 Data 对象进行更改时执行 put 操作:

public void changeData(UUID oldId, Foo newInfo) {
    Data oldData = map.get(oldId);
    Data newData = new Data(oldData, newInfo); // Constructor for demo only
    map.put(newData);
    saveToDatabase(newData);
}

B) 使用 volatile 字段、原子引用或最终并发字段使 Data 类对象可变但线程安全,并根据需要简单地修改对象:

public void changeData(UUID oldId, Foo newInfo) {
    Data data = map.get(id);
    data.changeSomething(newInfo);
    saveToDatabase(data);
}

C) 以上都不是

4

3 回答 3

7

A) 是更好的选择,原因有两个:

  1. 由于在您的场景中读取更频繁,您应该减少它们的开销。在这种情况下,添加额外的同步(例如volatile)对您不利。
  2. 通过使用带有额外自定义保护措施(可能有错误)的可变对象,您几乎无法通过使用ConcurrentHashMap.
于 2013-08-20T16:24:56.220 回答
3

只是一个想法。您说写入率很低,但为了论证,我们假设该方法有多个并发写入/调用changeData。然后,最后调用该方法的线程可能首先完成(在两种方法中)。

如果您的应用程序逻辑假定将遵守插入顺序,则可能会产生错误的结果。在这种情况下,方法的主体changeData是您的关键部分,根据定义意味着它不应该同时执行。

临界区定义对应用程序域语义和代码结构高度敏感,因此我无法真正判断该方法是否被视为临界区。通过变量的名称猜测,并假设您的地图是数据库中的用户数据缓存,我猜您可以忽略这个答案。但是仔细考虑一下:)

如果所有的写入都通过这个方法,这将是代码的草图(你可以使用非线程安全的映射实现):

public void changeData(UUID oldId, Foo newInfo) {
    synchronized(SomeClass.class) { // global lock
        //update logic
    }
}

当然,这只是一个草图来说明这一点。如果这问题所在,您很可能可以使用一些 Java 并发构造。

于 2013-08-20T17:11:43.120 回答
3

如果您可以选择创建一个不可变类,那么您的实现会更好#A:就地修改很难实现和维护。

有时走不可变的路线可能不是一种选择,因为需要对相对较大的对象进行频繁的修改。在这种情况下,您可能需要重新考虑将并发哈希映射应用于您的设计,因为它是同步的这一事实并没有给您带来太多优势。

于 2013-08-20T16:25:05.027 回答