3

在我的 Java 类中,我包含一个 Hashmap变量(类属性)并运行一些只写到该变量的线程,HashMap使用put():每次写入发生时,它都会存储一个唯一键(这是由设计完成的)。

类方法上的synchronized关键字只写是否足以满足安全条件?我的 HashMap 很简单,而不是ConcurrentHashMap?

4

3 回答 3

10

不,仅同步写入是不够的。必须将同步应用于对内存的读取和写入。

某个其他线程,在某个地方,某个时候,将需要读取映射(否则,为什么要有映射?),并且该线程需要同步以正确查看映射表示的内存。它们还需要同步以避免在更新地图状态时因地图状态中的瞬时不一致而绊倒。

举一个假设的例子,假设线程 1 写入 hashmap,其效果仅存储在 CPU 1 的 1 级缓存。然后线程 2,几秒钟后有资格运行并在 CPU 2 上恢复;它读取来自 CPU 2 的 1 级缓存的 hashmap - 它看不到线程 1 所做的写入,因为在写入和读取线程中的写入和读取之间没有内存屏障操作。即使线程 1 同步了写入,虽然写入的效果会被刷新到主内存,但线程 2 仍然看不到它们,因为读取来自一级缓存。所以同步写入只能防止写入冲突。

除了 CPU 缓存之外,JMM 还允许线程自己私下缓存数据,这些数据只需要在内存屏障处刷新到主内存(同步、具有一些特殊限制的易失性或在 JMM 5+ 中完成不可变对象的构造)。

要完全理解线程这个复杂的主题,您必须研究和研究 Java 内存模型,以及它对线程之间共享数据的影响。您必须了解“先发生”关系和内存可见性的概念,才能了解在当今多核 CPU 世界中共享数据的复杂性以及不同级别的 CPU 缓存。

如果您不想花时间理解 JMM,简单的规则是两个线程必须在某个地方/以某种方式在同一个对象上同步一个线程的写入和读取,以查看另一个线程的操作效果. 时期。请注意,这并不意味着对象上的所有写入和读取本身都必须同步。在一个线程中创建和配置对象然后将其“发布”到其他线程是合法的,只要发布线程和获取线程在同一对象上同步以进行移交。

于 2013-04-03T17:44:49.950 回答
1

您可以将synchronized修饰符添加到您的方法签名中,它应该没问题。我做了一个简单的例子来向你展示它的实际效果。您可以修改循环以设置任意数量的线程。

它会尝试添加相同的键n时间,如果您遇到并发问题,则映射中应该有重复的键。

class MyMap{

    private Map<String, Object> map;

    public MyMap(){
        map = new HashMap<String, Object>();
    }

    public synchronized void put(String key, Object value){
        map.put(key, value);
    }

    public Map<String, Object> getMap(){
        return map;
    }

}

class MyRunnable implements Runnable{

    private MyMap clazz;

    public MyRunnable(MyMap clazz){
        this.clazz = clazz;
    }

    @Override
    public void run(){
        clazz.put("1", "1");
    }

}

public class Test{

    public static void main(String[] args) throws Exception{
        MyMap c = new MyMap();

        for(int i = 0 ; i < 1000 ; i ++){
            new Thread(new MyRunnable(c)).start();
        }

        for(Map.Entry<String, Object> entry : c.getMap().entrySet()){
            System.out.println(entry);
        }
    }
}
于 2013-04-03T17:47:10.257 回答
1

write 方法足以保证线程安全,synchronized只要:

  • 您的类中没有其他方法允许修改底层哈希表;
  • 底层的哈希表没有以任何方式暴露,所以它不能被自己的方法修改(简单:构造一个私有实例);
  • 如果与写入方法同时使用,所有读取哈希表的方法也会同步。get()想象一下,如果在哈希映射被修改到一半时调用a 会发生什么。

如果您必须在写入哈希映射的同时读取哈希映射,那么最后一点很糟糕;ConcurrentHashMap在这种情况下使用。

如果您只有一堆并发写入哈希映射,然后只在一个线程中读取它,您的解决方案应该没问题。

于 2013-04-03T17:47:15.377 回答