当前位置: 首页 > 面试题库 >

以线程安全的方式延迟初始化Java映射

吕胤
2023-03-14
问题内容

我需要延迟初始化地图及其内容。到目前为止,我有以下代码:

class SomeClass {
    private Map<String, String> someMap = null;

    public String getValue(String key) {
        if (someMap == null) {
            synchronized(someMap) {
                someMap = new HashMap<String, String>();
                // initialize the map contents by loading some data from the database.
                // possible for the map to be empty after this.
            }
        }
        return someMap.get(key);  // the key might not exist even after initialization
    }
}

这显然不是线程安全的,就好像一个线程在someMapnull时出现,继续将字段初始化为,new HashMap并且当该字段仍将数据加载到映射中时,另一个线程执行a getValue并且在可能存在的情况下不获取数据。

当第一次getValue调用发生时,如何确保数据仅在地图中加载一次。

请注意,key所有初始化后,地图中可能不会存在。同样,在所有初始化之后,映射可能只是空的。


问题答案:

双重检查锁定

双重检查锁定需要完成几个步骤才能正常工作,您错过了其中两个。

首先,您需要将其someMap变成一个volatile变量。这样一来,其他线程将在更改后但完成更改后看到更改。

private volatile Map<String, String> someMap = null;

您还需要nullsynchronized块内进行第二次检查,以确保在等待进入同步区域时没有另一个线程为您初始化它。

    if (someMap == null) {
        synchronized(this) {
            if (someMap == null) {

待使用前不要分配

在生成映射时,请在temp变量中构造它,然后在最后分配它。

                Map<String, String> tmpMap = new HashMap<String, String>();
                // initialize the map contents by loading some data from the database.
                // possible for the map to be empty after this.
                someMap = tmpMap;
            }
        }
    }
    return someMap.get(key);

解释为什么需要临时地图。完成这一行someMap = new HashMap...后,该字段将someMap不再为null。这意味着其他调用get将看到它,并且从不尝试进入该synchronized块。然后,他们将尝试从地图中获取数据,而无需等待数据库调用完成。

通过确保分配到someMap同步块中的最后一步,可以防止这种情况的发生。

unmodifiableMap

正如评论中所讨论的,出于安全考虑,最好将结果保存为,unmodifiableMap因为将来的修改将不是线程安全的。对于从未公开的私有变量,这不是严格要求的,但是对于将来仍然更安全,因为它将阻止人们稍后进入并更改代码而没有意识到。

            someMap = Collections.unmodifiableMap(tmpMap);

为什么不使用ConcurrentMap?

ConcurrentMap使单个操作(即putIfAbsent)成为线程安全的,但它不满足此处的基本要求,即等到映射完全填充数据后才允许对其进行读取。

另外,在这种情况下,延迟初始化之后的Map不会再次被修改。这ConcurrentMap将为在此特定用例中不需要同步的操作增加同步开销。

为什么要 对此进行 同步?

没有理由。:)这是提出此问题的有效答案的最简单方法。

在私有内部对象上进行同步肯定是更好的做法。您已经在改进封装方面进行了权衡,以稍微增加内存使用量和对象创建时间。同步的主要风险this在于,它允许其他程序员访问您的锁对象,并可能尝试自己尝试对其进行同步。然后,这会导致他们的更新与您的更新之间发生不必要的争用,因此内部锁定对象会更安全。

实际上,尽管在许多情况下,单独的锁定对象是过大的。这是一个基于您的类的复杂性以及是否使用广泛而不是仅仅锁定on的简单性的判断调用this。如有疑问,您可能应该使用内部锁定对象并采取最安全的路线。

在课堂里:

private final Object lock = new Object();

在方法中:

synchronized(lock) {

对于java.util.concurrent.locks对象,在这种情况下它们不会添加任何有用的东西(尽管在其他情况下它们非常有用)。我们始终希望等到数据可用后,才能使用标准的同步块为我们提供所需的行为。



 类似资料:
  • 7.4.4 延迟初始化的bean 默认情况下,ApplicationContext实现在初始化过程中随即创建和配置所有单例bean。一般来说,这种预实例化是可取的,因为可以立即发现配置或周围环境中的错误,而不是在几个小时甚至几天以后。当这种行为不可取时,可以通过将bean定义标记为延迟初始化来阻止预实例化。延迟初始化的bean告诉IoC容器,当bean首次被请求时而不是在启动时创建一个实例。 在X

  • 问题内容: 我正在使用静态代码块来初始化我拥有的注册表中的某些控制器。因此,我的问题是,我可以保证在首次加载该类时,该静态代码块仅被绝对调用一次吗?我知道我不能保证何时将调用此代码块,我猜是在Classloader首次加载时。我意识到我可以在静态代码块中的类上进行同步,但是我猜这实际上是怎么回事? 简单的代码示例将是; 还是我应该这样做? 问题答案: 是的,Java静态初始化器是线程安全的(使用第

  • 问题内容: 我试图基于初始化按需持有人习惯用法创建一个线程安全的单例类。这是我的代码 我的期望是以线程安全的方式初始化ExecutorService,并且那里只有一个实例(静态)。 这段代码是实现了这一目标-还是需要任何更改? 问题答案: 根据SEI 指南,您的方法很好。 但是,由于我们有枚举,因此可以使用枚举的简单方法: 而且,如果您想变得真正聪明,还可以定义枚举实现的接口。因为这样您以后可以

  • 我有maploader,它使用索引进行查询 > maploader 主 这个工作绝对精细的地图不会加载,直到我第一次接触地图。 但是当我为地图添加索引的时候,地图就会被加载,而不管是不是触摸地图。在Hazelcast文档中,MapStoreConfig类中的InitialLoadMode配置参数有两个值:LAZY和eager。如果InitialLoadMode设置为LAZY,则在映射创建期间不加载

  • 问题内容: 我想延迟控制器的初始化,直到从服务器收到必要的数据为止。 我找到了针对Angular1.0.1的解决方案:延迟AngularJS路由更改,直到加载模型以防止闪烁,但无法使其与Angular1.1.0一起使用 模板 ​ JavaScript http://jsfiddle.net/dTJ9N/1/ 问题答案: 由于$ http返回了promise,因此创建自己的deferred仅在htt

  • 下面我分享了我的代码,我试图使用线程安全的Nashorn作为脚本引擎来评估简单的数学公式。公式将类似于“a*b/2”,其中a 我需要知道这种方法是否有助于使Nashorn线程在这个用例中安全。我在Attila的回答中读到,我们可以在线程之间共享脚本引擎对象,因为它们是线程安全的。 对于bindings和eval,因为我们正在为每次执行evaluate创建新线程,每个线程都有自己的bindings对