【问题标题】:The right way to synchronize access to read only map in Java在Java中同步访问只读地图的正确方法
【发布时间】:2011-11-11 15:02:54
【问题描述】:

我正在编写一个类似DatabaseConfiguration 的类,它从数据库中读取配置,我需要一些关于同步的建议。 例如,

public class MyDBConfiguration{
   private Connection cn;
   private String table_name;
   private Map<String, String> key_values = new HashMap<String,String>();
   public MyDBConfiguration (Connection cn, String table_name) {
      this.cn = cn;
      this.table_name = table_name;
      reloadConfig();
   }
   public String getProperty(String key){
       return this.key_values.get(key);
   }
   public void reloadConfig() {
      Map<String, String> tmp_map = new HashMap<String,String> ();
      // read  data from database
      synchronized(this.key_values)
      {
          this.key_values = tmp_map;
      }
   }
}

所以我有几个问题。
1.假设属性是只读的,我是否在getProperty中使用了synchronize
2.在reloadConfig中做this.key_values = Collections.synchronizedMap(tmp_map)有意义吗?

谢谢。

【问题讨论】:

    标签: java multithreading synchronization


    【解决方案1】:

    如果多个线程要共享一个实例,您必须使用某种类型的同步。

    需要同步主要有两个原因:

    • 可以保证某些操作是原子的,所以系统会保持一致
    • 它保证每个线程在内存中看到相同的值

    首先,由于您将reloadConfig() 公开,您的对象看起来并不是真正的不可变。如果对象真的是不可变的,也就是说,如果在初始化其值之后它们不能改变(这是共享对象中的期望属性)。

    由于上述原因,您必须同步对映射的所有访问:假设一个线程正在尝试从中读取,而另一个线程正在调用reloadConfig()。坏事会发生。

    如果确实如此(可变设置),则必须在读取和写入中同步(原因很明显)。 线程必须在单个对象上同步(否则没有同步)。保证所有线程在同一个对象上同步的唯一方法是在对象本身或正确发布、共享的锁中同步,如下所示:

    // synchronizes on the in instance itself:
    class MyDBConfig1 {
      // ...
      public synchronized String getProperty(...) { ... }
      public synchronized reloadConfig() { ... }
    }
    
    // synchronizes on a properly published, shared lock:
    class MyDBConfig2 {
      private final Object lock = new Object();
      public String getProperty(...) { synchronized(lock) { ... } }
      public reloadConfig() { synchronized(lock) { ... } }
    }
    

    正确发布这里由 final 关键字保证。很微妙:它保证了这个字段的值在初始化之后对每个线程都是可见的(没有它,一个线程可能会看到lock == null,并且会发生不好的事情)。

    您可以使用(正确发布的)ReadWriteReentrantLock 来改进上面的代码。如果您担心,它可能会稍微提高并发性。

    假设您的意图是使MyDBConfig 不可变,您不需要序列化对哈希映射的访问(也就是说,您不一定需要添加同步关键字)。您可能会提高并发性。

    首先,将reloadConfig()设为私有(这将表明,对于这个对象的消费者来说,它确实是不可变的:他们看到的唯一方法是getProperty(...),顾名思义,它不应该修改实例)。

    然后,您只需要保证每个线程都能在哈希映射中看到正确的值。为此,您可以使用上面介绍的相同技术,也可以使用 volatile 字段,如下所示:

    class MyDBConfig {
      private volatile boolean initialized = false;
      public String getProperty(...) { if (initialized) { ... } else { throw ... } }
      private void reloadConfig() { ...; initialized = true; }
      public MyDBConfig(...) { ...; reloadConfig(); }
    }
    

    volatile 关键字非常微妙。易失性写入和易失性读取具有 happens-before 关系。一个 volatile 写入被称为 happen-before 相同(volatile)字段的后续 volatile 读取。这意味着在(按程序顺序)易失性写入之前已修改的所有内存位置在它们执行了相同(易失性)字段的后续易失性读取之后对每个其他线程都是可见的。

    在上面的代码中,您将true 写入到 volatile 字段之后所有值都已设置。然后,读取值的方法 (getProperty(...)) 通过执行同一字段的易失性读取开始。那么这个方法保证能看到正确的值。

    在上面的例子中,如果你没有在构造函数完成之前发布实例,保证不会在方法getProperty(...)中抛出异常(因为在构造函数完成之前,你已经写了@987654335 @ 初始化)。

    【讨论】:

    • 感谢您的回答。如果我只是按照 John Vint 的提议声明基础地图 volatile ,它是否也有效?
    • 不,一点也不。 volatile 只保证: (1) volatile 字段的值对所有线程在任何时候都是可见的; (2) 如果一个线程修改了一些其他内存位置,然后写入一个 volatile 字段,然后另一个线程从同一个 volatile 字段读取,那么第二个线程将看到所有其他修改的内存位置的当前值。 Vint 建议的方法行不通,因为保存散列映射的 volatile 字段将被写入 一次,并且在将值写入它之前。这些值可能永远不会被其他线程看到。
    • @Bruno 如果地图需要自动填充,那么您需要同步所有人口和分配。但是,如果地图只需要像他在他的示例中尝试的那样正确发布,那么将字段声明为 volatile 绝对会起作用。 “这些值可能永远不会被其他线程看到。” 哪些值可能看不到,地图中的那些?因为那不是真的。
    • Vint,它不会像你建议的那样工作,一切都在我上面的评论中解释了。原因是您将值写入地图之前执行了易失性写入。对映射的后续访问,即使他们在访问映射之前执行易失性读取,存储在映射中的值也可能不可用。要纠正这个问题,您需要首先在局部变量中实例化一个映射,然后填充它,然后才将此局部映射分配给 volatile 字段。在这种情况下,易失性写入将在设置值之后发生。然后,可以对地图进行易失性读取。
    • 如果您仍然不同意或不理解您的建议为何行不通,那么您必须研究一下内存模型。一个很好的起点是 Java Concurrency in Practice,作者 Brian Goetz。
    【解决方案2】:
    1. 假设 key_values 不会是 putreloadConfig 之后,您将需要同步对映射的读取和写入的访问。您仅在分配上同步就违反了这一点。您可以通过删除同步块并将 key_values 分配为 volatile 来解决此问题。

    2. 1234563 )。

    注意:此外,您永远不应该同步会更改的字段。结果非常难以预测。

    编辑:关于其他答案。强烈建议所有共享的可变数据必须同步,因为效果是不确定的。 key_values 字段是共享的可变字段,必须同步对其的分配。

    编辑:并澄清与布鲁诺雷斯的任何混淆。如果您仍然填写 tmp_map 并在完成填写后将其分配给 this.key_valuesvolatile 字段将是合法的,它看起来像:

       private volatile Map<String, String> key_values = new HashMap<String,String>();
    
      ..rest of class 
    
       public void reloadConfig() {
          Map<String, String> tmp_map = new HashMap<String,String> ();
          // read  data from database
    
          this.key_values = tmp_map;
       }
    

    您仍然需要相同的样式,否则就像 Bruno Reis 指出的那样,它不是线程安全的。

    【讨论】:

    • 感谢您的回答。只是为了确保我能很好地理解你,可靠的方法是: 1. 添加一个成员,例如,Object key_value_lock = new Object()。 2. 每次我访问底层地图(即使是读取),都在synchronize (key_value_lock)block 内进行。对吗?
    • 是的,这是一种安全的执行方式。同样,我宁愿做private volatile Map&lt;String,String&gt; key_values=... 易失性负载比同步负载快得多。
    • 另外,作为线程安全的旁注。如果可以将对象声明为最终对象,则最好这样做。 final Object key_value_lockfinal Connection cn 等等。
    • 再次感谢,我会尝试这两种方法。我有点不愿意在 Java 中使用volatile
    • 锁对象必须是最终的,否则它会被破坏(其他线程可能会看到该字段为空)。
    【解决方案3】:

    我想说,如果你保证没有代码会在结构上修改你的地图,那么就不需要同步它。

    如果多个线程同时访问一个哈希映射,并且至少有一个 的线程在结构上修改映射,它必须同步 外部。 http://download.oracle.com/javase/6/docs/api/java/util/HashMap.html

    您显示的代码仅提供对地图的读取权限。客户端代码无法进行结构修改。

    由于您的 reload 方法会更改临时地图,然后将 key_values 更改为指向新地图,我再说一遍,不需要同步。可能发生的最糟糕的情况是有人从旧地图副本中读取。

    我现在要低着头等待反对票;)

    编辑

    正如布鲁诺所说,美中不足的是继承。如果你不能保证你的类不会被子分类,那么你应该更加防御。

    编辑

    只是参考OP提出的具体问题......

    1. 假设属性是只读的,我是否在 getProperty 中使用了同步?
    2. 在 reloadConfig 中做 this.key_values = Collections.synchronizedMap(tmp_map) 有意义吗?

    ...我真的很想知道我的答案是否错误。所以我暂时不会放弃并删除我的答案;)

    【讨论】:

    • @sudocode。你的答案仍然是错误的。错误的原因不是 HashMap 内部的可变性,而是 HashMap 字段的可变性。即使在另一个线程已经写入该字段之后,一个线程也有可能进入并看到 key_values 映射的过时版本。为了多线程编程的正确性,需要同步访问共享可变数据的所有读写。
    • 而且由于该字段不是final,线程仍然可以初始化 MyDBConfiguration 并可能看到一个空的 key_values 映射。双重检查锁定习语是该属性的典型违反者。
    • @John “可能发生的最糟糕的情况是有人从旧地图副本中读取。”这个说法不正确吗?
    • 最糟糕的实际上是 NullPointerException。但是,如果您这样做了if(this.key_values != null)...,那么是的,最糟糕的是阅读旧地图。请记住,尽管这种推理并不适用于所有作业。例如,private long someLong = 10; 如果您在一个线程中执行了someLong = 5000000; 而在另一个线程中执行了someLong = 10000000;,则阅读线程可能会看到两者的组合,您可以在此处阅读以获取有关stackoverflow.com/questions/3463658/… 的更多信息
    • 我们正在审查的代码中的 key_values 究竟如何才能为空?
    猜你喜欢
    • 1970-01-01
    • 2010-11-28
    • 1970-01-01
    • 2013-04-15
    • 1970-01-01
    • 2016-06-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多