懒惰地以线程安全的方式初始化Java映射 [英] Lazily initialize a Java map in a thread safe manner

查看:143
本文介绍了懒惰地以线程安全的方式初始化Java映射的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要懒洋洋地初始化地图及其内容。我现在有以下代码:

I need to lazily initialize a map and its contents. I have the below code till now:

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
    }
}

这显然不是线程安全的,好像一个线程在 someMap 为空时出现,继续将字段初始化为 new HashMap 并且当它仍然在地图中加载数据时,另一个线程会执行 getValue ,并且在可能存在数据时不会获取数据。

This is obviously not thread-safe as if one thread comes when someMap is null, goes on to initialize the field to new HashMap and while its still loading the data in the map, another thread does a getValue and doesn't get the data when one might have existed.

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

How can I make sure that the data is loaded in the map only once when the first getValue call happens.

请注意,在所有初始化之后,地图中可能不存在。此外,在所有初始化之后,地图可能只是空的。

Please note that it's possible that the the key won't exist in the map after all the initialization. Also, it's possible that the map is simply empty after all the initialization.

推荐答案

双重检查锁定

双重检查锁定需要完成几个步骤才能正常工作,你缺少其中两个。

Double check locking requires several steps all to be completed in order to work properly, you are missing two of them.

首先,您需要将 someMap 转换为 volatile 变量。这样,其他线程会在完成更改后看到对其进行的更改。

First you will need to make someMap into a volatile variable. This is so that other threads will see changes made to it when they are made but after the changes are complete.

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

您还需要再次检查 null synchronized 块内,以确保在您等待进入同步区域时,另一个线程尚未为您初始化。

You also need a second check for null inside the synchronized block to make sure that another thread hasn't initialized it for you while you were waiting to enter the synchronized area.

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

在准备好使用之前不要分配

在你生成的map中,在temp变量中构造它,然后在最后分配它。

In your generation of the map construct it in a temp variable then assign it at the end.

                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 就不再为空。这意味着对 get 的其他调用将会看到它并且从不尝试输入 synchronized 块。然后他们将尝试从地图获取而不等待数据库调用完成。

To explain why the temporary map is required. As soon as you complete the line someMap = new HashMap... then someMap is no longer null. That means other calls to get will see it and never try to enter the synchronized block. They will then try to get from the map without waiting for the database calls to complete.

通过确保赋值给 someMap 是同步块中阻止这种情况发生的最后一步。

By making sure the assignment to someMap is the last step within the synchronized block that prevents this from happening.

unmodifiableMap

正如评论中所讨论的那样,为了安全起见,最好将结果保存在 unmodifiableMap 中,因为将来的修改不是线程安全的。对于从未暴露过的私有变量来说,这并不是严格要求的,但它对未来仍然更安全,因为它会阻止人们稍后进入并更改代码而不会意识到。

As discussed in the comments, for safety it would also be best to save the results in an unmodifiableMap as future modifications would not be thread safe. This is not strictly required for a private variable that is never exposed, but it's still safer for the future as it stops people coming in later and changing the code without realizing.

            someMap = Collections.unmodifiableMap(tmpMap);

为什么不使用ConcurrentMap?

ConcurrentMap 使个别操作(即 putIfAbsent )线程安全,但它不符合基本原理这里要求等到地图完全填充数据才允许从中读取数据。

ConcurrentMap makes individual actions (i.e. putIfAbsent) thread-safe but it does not meet the fundamental requirement here of waiting until the map is fully populated with data before allowing reads from it.

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

Additionally in this case the Map after the lazy initialization is not being modified again. The ConcurrentMap would add synchronization overhead to operations that in this specific use case do not need to be synchronized.

为什么要同步这个

Why synchronize on this?

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

There is no reason. :) It was just the simplest way to present a valid answer to this question.

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

It would certainly be better practice to synchronize on a private internal object. You have improved encapsulation traded off for marginally increased memory usage and object creation times. The main risk with synchronizing on this is that it allows other programmers to access your lock object and potentially try synchronizing on it themselves. This then causes un-needed contention between their updates and yours, so an internal lock object is safer.

实际上,虽然在许多情况下单独的锁定对象过度杀伤。这是一个基于你的类的复杂性的判断调用,以及使用多少来反对只是锁定这个的简单性。如果有疑问,你应该使用内部锁定对象并采取最安全的路线。

In reality though a separate lock object is overkill in many cases. It's a judgement call based on the complexity of your class and how widely is is used against the simplicity of just locking on this. If in doubt you should probably use an internal lock object and take the safest route.

在课堂上:

private final Object lock = new Object();

方法中:

synchronized(lock) {

至于 java .util.concurrent.locks 对象,在这种情况下它们不会添加任何有用的东西(尽管在其他情况下它们非常有用)。我们总是希望等到数据可用,这样标准的同步块就能完全满足我们所需的行为。

As for java.util.concurrent.locks objects, they don't add anything useful in this case (although in other cases they are very useful). We always want to wait until the data is available so the standard synchronized block gives us exactly the behavior we need.

这篇关于懒惰地以线程安全的方式初始化Java映射的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆