【问题标题】:When and how should I use additional synchronization of ConcurrentHashMap?我应该何时以及如何使用 ConcurrentHashMap 的额外同步?
【发布时间】:2026-01-18 19:20:03
【问题描述】:

在使用 ConcurrentHashMap 时,我需要知道何时应该在我的代码中添加一些同步块。假设我有一个类似的方法:

private static final ConcurrentMap<String, MyObjectWrapper> myObjectsCache = new ConcurrentHashMap<>(CACHE_INITIAL_CAPACITY);

    public List<MyObject> aMethod(List<String> ids, boolean b) {
        List<MyObject> result = new ArrayList<>(ids.size());
        for (String id : ids) {
            if (id == null) {
                continue;
            }
            MyObjectWrapper myObjectWrapper = myObjectsCache.get(id);
            if (myObjectWrapper == null) {
                continue;
            }
            if (myObjectWrapper.getObject() instanceof MyObjectSub) {
                ((MyObjectSub) myObjectWrapper.getObject()).clearAField();
                myObjectWrapper.getObject().setTime(System.currentTimeMillis());
            }
            result.add(myObjectWrapper.getObject());
            if (b) {
                final MyObject obj = new MyObject(myObjectWrapper.getObject());
                addObjectToDb(obj);
            }
        }
        return result;
    }

我应该如何有效地使这个方法并发? 我认为“get”是安全的,但是一旦我从缓存中获取值并更新缓存对象的字段 - 可能会出现问题,因为另一个线程可以获得相同的包装器并尝试更新相同的底层对象......我应该添加同步?如果是这样,那么我应该从“get”同步到循环迭代结束还是整个循环?

当需要对循环内的映射键/值等进行更多操作时,也许有人可以分享一些更具体的正确和有效使用 ConcurrentHashMap 的指南等......

我将不胜感激。

编辑: 这个问题的一些背景: 我目前正在重构生产代码中的一些 dao 类,其中一些类使用 HashMaps 来缓存从数据库中检索到的数据。所有使用缓存(用于写入或读取)的方法都将其全部内容放在同步(缓存)块中(安全吗?)。我在并发方面没有太多经验,我很想利用这个机会学习。我天真地将 HashMaps 更改为 ConcurrentHashMaps,现在想在需要的地方删除同步块。所有缓存都用于写入和读取。所提出的方法基于我已更改的方法之一,现在我正在尝试了解何时以及在何种程度上同步。 clearAField 方法只是更改包装的 POJO 对象的其中一个字段的值,然后 addObjectToDb 尝试将该对象添加到数据库中。

其他示例是重新填充缓存:

public void findAll() throws SQLException{
    // get data from database into a list
    List<Data> data=getAllDataFromDatabase();
    cacheCHM.clear();
    cacheCHM.putAll(data);
} 

在这种情况下,我应该将 clear 和 putAll 放在 synchronize(cacheCHM) 块中,对吗?

我尝试查找并阅读一些关于正确有效地使用 CHM 的帖子/文章,但大多数都处理单个操作,没有循环等。我发现最好的是: http://www.javamadesoeasy.com/2015/04/concurrenthashmap-in-java.html

【问题讨论】:

  • 这在很大程度上取决于您的应用程序逻辑。
  • 如果您需要更新检索到的值以实现线程安全,那么您必须在对象本身上进行同步。 ConcurrentHashMap 只保护映射本身的结构(即键与值的关系),而不是包含的值。
  • 我同意@Jim,并发映射只会保护结构(即键与值的关系)。我想再添加一件事,根据上面的代码上下文,您只读取值,即 myObjectsCache.get(id),因此为此您甚至可能不需要并发映射,直到您调用map.put().
  • 所以我应该使用 synchronize(myObjectWrapper) 块并将包装器上的所有操作放入其中,或者我是否还需要将从缓存中检索对象放入同步块中?还是整个循环?但是,从我读到的内容来看,您应该尽可能少地使用同步块,所以最后一个不是一个好主意,对吧?

标签: java multithreading concurrenthashmap


【解决方案1】:

你没有提到你期望在你的应用中发生什么并发,所以我假设你有多个线程调用aMethod没有别的

您只需调用一次 ConcurrentHashMap:myObjectsCache.get(id),这很好。事实上,由于没有将数据写入您的 objectCache [参见上面的假设],您甚至不需要 ConcurrentHashMap!任何不可变的集合都可以。你最后有一个可疑的行:addObjectToDb(obj),这个方法也会影响你的缓存吗?如果是这样,它仍然是安全的(可能我们必须看到方法才能确定),但你肯定需要 ConcurentHashMap。

危险就在你改变对象的地方,这里:

myObjectWrapper.getObject().clearAField();
myObjectWrapper.getObject().setTime(System.currentTimeMillis());

多个线程可以同时在同一个对象上调用这些方法。在不知道这些方法的作用的情况下,我们不能说这是否安全。如果这些方法都标记为已同步,或者如果您注意确保这些方法同时运行是安全的,那么您就可以了(但请注意,这些方法的运行顺序可能与您可能直观地期望的不同! )。如果您不那么小心,则可能会损坏数据。

【讨论】:

  • 抱歉没有说得很具体。我的一些 dao 类中有缓存映射,它们存储从 sql 数据库检索到的数据。这些缓存被写入和读取,也可能发生对缓存对象的一些操作。这些类将 HashMaps 用于缓存,并将使用缓存的方法的所有内容与映射上的锁同步。我想重构代码以提高效率并保持缓存凝聚力。示例中的方法 clearAField 和 setTime 不同步,它们是 pojo 的普通设置器。
  • 您的问题范围开始听起来很广泛。简而言之,使您的世界线程安全的最简单方法是在并发安全容器中使用不可变对象。当你开始改变容器内的对象(不复制它们)时,就像你在这里所做的那样,你将自己暴露在一堆潜在的问题中。
  • 所以我应该同步缓存上的所有非原子操作,如果我在检索缓存对象后更改缓存对象(删除的对象也算?还是只有“获取”的对象?) - 我应该包含该代码在同步块中(我认为我不能使缓存的对象不可变)?如您所见,我正在找人牵着我的手……因为我需要确保我的更改是有效的。
  • 1.如果一个线程在缓存中看到“旧”对象而另一个线程看到“新”对象,这有关系吗?通常这无关紧要,但如果确实如此,您需要广泛的锁。 2. 如果这无关紧要,那么只要在将对象放入缓存(即不可变对象)后从不更改对象,您通常就可以了。如果您需要更新缓存,只需创建一个全新的对象并丢弃旧的对象。无需额外同步。
  • 您能否展示我的问题中的第一种方法应该如何看待您的两个观点(尽管看到“旧”对象应该不是问题)。
【解决方案2】:

线程安全和缓存的更好方法是使用immutable objects。如果 MyObjectSub 类是不可变的,这就是它的样子[不知道为什么需要包装器 - 我会完全忽略它]:

//Provided by way of example. You should consider generating these 
//using http://immutables.github.io/ or similar
public class MyImmutableObject {
    //If all members are final primitives or immutable objects 
    //then this class is threadsafe.
    final String field;
    final long time;

    public MyImmutableObject(String field, long time) {
        this.field = field;
        this.time = time;
    }

    public MyImmutableObject clearField() {
        //Since final fields can never be changed, our only option is to 
        //return a copy.
        return new MyImmutableObject("", this.time);
    }

    public MyImmutableObject setTime(long newtime) {
        return new MyImmutableObject(this.field, newtime);
    }
}

如果你的对象是不可变的,那么线程安全就简单多了。你的方法看起来像这样:

public List<Result> typicialCacheUsage(String key) {
    MyImmutableObject obj = myObjectsCache.get(key);

    obj = obj.clearField();
    obj = obj.setTime(System.currentTimeMillis());

    //If you need to put the object back in the cache you can do this:
    myObjectsCache.put(key, obj);

    List<Result> res = generateResultFromObject(obj);
    return res;
}

【讨论】:

  • 最好将此添加到现有答案中,而不是将其作为单独的答案发布 - 就其本身而言,这不是问题的答案。
最近更新 更多