如果多个线程要共享一个实例,您
必须使用
某种同步方式。
同步主要有两个原因:
- 它可以保证某些操作是原子的,从而系统保持一致性
- 它保证每个线程在内存中看到相同的值
首先,由于您将
reloadConfig()
设置为public,因此您的对象实际上并不是不可变的。如果对象确实是不可变的,也就是说,在初始化其值后,它们不能更改(这是在共享的对象中具有的
期望属性)。
出于以上原因,您
必须同步对地图的所有访问:假设一个线程正在尝试从地图中读取数据,而另一个线程正在调用
reloadConfig()
。那么会出现问题。
如果这确实是情况(可变设置),则必须进行读写同步(出于明显的原因)。
线程必须在单个对象上同步(否则就没有同步)。为了确保所有线程都在相同的对象上同步,您必须在对象本身上同步或在适当发布和共享锁上同步,例如:
class MyDBConfig1 {
public synchronized String getProperty(...) { ... }
public synchronized reloadConfig() { ... }
}
class MyDBConfig2 {
private final Object lock = new Object();
public String getProperty(...) { synchronized(lock) { ... } }
public reloadConfig() { synchronized(lock) { ... } }
}
正确的发布仅由 final 关键字来保证。这很微妙:它保证了该字段在初始化后对每个线程都是可见的(如果没有它,线程可能会看到 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
关键字非常微妙。Volatile 写和 volatile 读之间具有
先行发生关系。所谓 volatile 写在同一 (volatile) 字段的后续 volatile 读之前
先行发生。这意味着,在 volatile 写之前已经修改的所有内存位置(按程序顺序)在它们执行后的任何其他线程中都是可见的,并且在它们执行后的后续 volatile 读取相同的 (volatile) 字段时也是如此。
在上面的代码中,您在设置完所有值后将
true
写入 volatile 字段。然后,读取值的方法 (
getProperty(...)
) 通过执行相同字段的 volatile 读取开始执行。然后,该方法保证能够看到正确的值。
在上面的示例中,如果您在构造函数完成之前不发布实例,则保证不会在方法
getProperty(...)
中抛出异常(因为在构造函数结束之前,您已经将 initialized 写入了
true
)。
Map<String, String> tmp_map = new HashMap<String,String> (); // read data from database
填充tmp_map,然后将tmp_map分配给key_values是完全合法的,并符合happens-before关系。 - John Vint