Singleton class 在多线程环境中保留上一次迭代的值,重置可能会导致 NullPointerException

Singleton class persists the values from previous iteration in multithread environment and resetting may result in NullPointerException

我有一个单例 class,其他各种 class 将使用它在代码执行期间通过访问单个实例来填充 Map。这里的一切似乎都很好。但是当我第二次再次执行代码时,上一次迭代的值仍然存在。

我知道这是因为创建单个实例的单例 class 造成的。我不明白如何重置它。

以下是我的代码:

public class Namespace {

    @Getter
    private Map<String, String> namespaces = null;
    private static volatile Namespace instance;
    private static Object mutex = new Object();

    public static Namespace getInstance() {
        Namespace result = instance;
        if (result == null) {
            synchronized (mutex) {
                result = instance;
                if (result == null)
                    instance = result = new Namespace();
            }
        }
        return result;
    }

    public Namespace() {
        namespaces = new HashMap<>();
    }
    
    public void namespacePopulator(String namespaceURI, String prefix) {
        namespaces.put(namespaceURI, prefix);
    }

    public void instanceReset() {
        instance = null;
    }
}

以下是我访问这些方法的方式: 在程序执行开始时,我正在创建 class 的实例。只是为了重置 Map 并创建一个新实例。

Namespace namespace = new Namespace();

然后我像这样填充地图:

Namespace.getInstance().namespacePopulator("Field1", "Value1");

我知道我可以在执行结束时调用 instanceReset() 方法,但我 运行 这个程序在多线程环境中,如果任何线程进行调用instanceReset() 其他线程将开始抛出 NullPointerException。

我需要一些解决方法或建议来处理这种情况。我想知道的是如何通过使用 Non-StaticMulti-thread 方法共享实例来从各种不同的 classes 和模块填充 Map

我看到了关于单例的各种示例,但它们似乎都遵循几乎相同的过程。我对单例方法或任何其他专用方法不是很挑剔。我想要的只是处理这个问题的最好和有效的方法。欢迎任何解决方法或想法。提前致谢。

使用 ConcurrentHashMapCollections.synchronizedMap,不要将它们设置为 null,而 instanceReset,请为地图设置 map.clear()

最好的解决方案是:如果你需要可变的东西,就不要使用单例。

我们在使用单例时总是要加倍小心class。单例对象可以很容易地在线程之间共享,这就是单例对象使用起来很危险的原因。这个问题是可能导致数据损坏的代码的完美示例,但事实并非如此,该代码也是错误编码实践的完美示例。

问题编号 1 -

Namespace namespace = new Namespace();

单例必须有一个私有的构造函数。 public 构造函数将破坏 class 的单例性质。每次创建 Namespace 对象时,map namespaces 都会初始化为 null。函数 namespacePopulator 将在 null Map 上工作,这将抛出 NullPointerException。所以将构造函数标记为私有。

问题编号 2 -

    public void instanceReset() {
      instance = null;
    }

函数instanceReset 破坏了编写精美的构造函数的非常方面。函数 instanceReset 不受互斥量保护。让我们考虑线程 T1 正在处理 getInstance 函数,但同时线程 T2 完成了 instanceReset 操作,我们无法预测线程 T1 是否应该从下面的列表中获取任何东西。

 1. A new instance 
 2. An old instance 
 3. A NULL 

问题编号 3 -

为什么我们需要 instanceReset 函数?只有当我们确定在应用程序的整个生命周期中只需要 class 的一个对象时,我们才应该使用 Singleton class 。函数instanceReset会导致内存泄漏。例如,假设线程 T1 获得命名空间的旧实例(在调用 instanceReset 之前创建的实例),该实例永远不会变为 null,也永远不会被垃圾回收。这可能会发生多次并且多个实例将保留在内存中并且不会被垃圾收集。即使向 instanceReset 函数添加互斥量也无法解决内存泄漏问题。

解决方案-

public class Namespace {

@Getter
private Map<String, String> namespaces = null;
private static volatile Namespace instance;
private static Object mutex = new Object();

public static Namespace getInstance() {
    Namespace result = instance;
    if (result == null) {
        synchronized (mutex) {
            result = instance;
            if (result == null)
                instance = result = new Namespace();
        }
    }
    return result;
}

private Namespace() { // Never make this public
    namespaces = new HashMap<>();
}

public void synchronized namespacePopulator(String namespaceURI, String prefix) 
{  // add synchronized keyword.
    namespaces.put(namespaceURI, prefix);
}

//public void instanceReset() { Never nullify a singleton instance this way. 
//    instance = null;
//}

public void synchronized namespaceReset() {
    namespaces.clear();
}

}

现在我们可以从任何线程调用 namespaceReset。我们必须牢记一件事,此更改将反映在线程中。

非常感谢大家的回复。我咨询了我的导师,我们能够做这样的事情。发布答案,因为它可能对将来的人有用:

这似乎根据线程工作并填充特定的地图。确保在应用程序开始时清除地图,这样每次我们都会有一个空地图,如果在执行过程中出现问题,那么地图将被填满一半,因此在代码开始时清除将避免所有这些问题,我们将保证有空地图。

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Namespaces {
    private static final ThreadLocal < Map < String, String >> NAMESPACE_MAPS = new ThreadLocal < Map < String, String >> () {

        @Override
        protected Map < String, String > initialValue() {
            return new HashMap < > ();
        }
    };

    private static Namespaces instance = new Namespaces();
    public static Namespaces getInstance() {
        return instance;
    }

    public synchronized void namespacePopulator(String namespaceURI, String prefix) {
        NAMESPACE_MAPS.get().put(namespaceURI, prefix);

    }

    public synchronized void namespaceReset() {
        NAMESPACE_MAPS.get().clear();
    }

    public Map < String, String > getNamespaces() {
        return NAMESPACE_MAPS.get();
    }
}