在Java中同步访问只读映射的正确方法

时间:2011-11-11 15:02:54

标签: java multithreading synchronization

我正在写一个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.假设属性是只读的,我是否在synchronize中使用getProperty? 2.在this.key_values = Collections.synchronizedMap(tmp_map)中执行reloadConfig是否有意义?

谢谢。

3 个答案:

答案 0 :(得分:5)

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

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

  • 它可以保证某些操作是 atomic ,因此系统将保持一致
  • 它保证每个线程在内存中看到相同的值

首先,由于您将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) { ... } }
}

正确发布此处由最终关键字保证。它很微妙:它保证了初始化后每个线程都可以看到这个字段的值(没有它,一个线程可能会看到lock == null,并且会发生坏事。)

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

假设您的目的是使MyDBConfig不可变,则不需要序列化对哈希映射的访问(也就是说,您不一定需要添加synchronized关键字)。您可以改善并发性。

首先,将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关键字非常微妙。易失性写入和易失性读取具有 发生之前 关系。易失性写入被称为 发生在 之后的相同(易失性)字段的后续易失性读取。这意味着,在执行相同(易失性)字段的子序列易失性读取之后,所有在(按程序顺序)易失性写入之前修改的所有内存位置对于每个其他线程都是可见的。

在上面的代码中,在设置了所有值之后,将true写入易失性字段。然后,读取值(getProperty(...))的方法开始于执行相同字段的易失性读取。然后,此方法可以保证看到正确的值。

在上面的示例中,如果在构造函数完成之前未发布实例,则可以保证在方法getProperty(...)中不会抛出异常(因为在构造函数完成之前,您已经编写了true初始化)。

答案 1 :(得分:3)

  1. 假设key_values在put之后不会是reloadConfig,您将需要同步访问地图的读取和写入。您只是通过同步分配来违反此规定。您可以通过删除synchronized块并将key_values指定为volatile来解决此问题。

  2. 由于HashMap只是有效读取,我不会分配Collections.synchronizedMap而不是Collections.unmodifiableMap(这不会影响Map本身,只是禁止意外put别人可能使用这个课程。)

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

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

    编辑:并清除与Bruno Reis的任何混淆。如果您仍然填写volatile,那么tmp_map字段将是合法的,并且在填写完成后将其分配给this.key_values它看起来像:

       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指出它不会是线程安全的。

答案 2 :(得分:-1)

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

  

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

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

由于您的重新加载方法会更改临时地图,然后将key_values更改为指向新地图,因此我再说不需要同步。最糟糕的情况是有人从地图的旧副本中读取。

我要低下头等待现在的下跌;)

修改

正如布鲁诺所说,美中不足的是遗传。如果你不能保证你的班级不会被分类,那么你应该更加防守。

修改

只是回顾一下OP提出的具体问题......

  
      
  1. 假设属性是只读的,我是否在getProperty中使用了同步?
  2.   
  3. 在reloadConfig中执行此操作是否有意义.key_values = Collections.synchronizedMap(tmp_map)?
  4.   

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