简体   繁体   English

同步块中的易变变量分配结果为空

[英]Volatile variable assignment in a synchronized block results in null

here's a class: 这是一堂课:

public class Refreshable<T> {
    private final Object lock = new Object();

    private final Supplier<Object>[] dynamicSettings;
    private final Supplier<T> constructor;

    private volatile Object[] instanceKey;
    private volatile T instance;

    @SafeVarargs
    public Refreshable(Supplier<T> constructor, Supplier<Object>... dynamicSettings) {
        this.constructor = constructor;
        this.dynamicSettings = dynamicSettings;
    }

    public T getInstance() {
        Object[] currentKey = getCurrentKey();
        if (!Arrays.equals(currentKey, instanceKey)) {
            synchronized (lock) {
                if (!Arrays.equals(currentKey, instanceKey)) {
                    instanceKey = currentKey;
                    instance = constructor.get();
                }
            }
        }
        return instance;
    }

    private Object[] getCurrentKey() {
        return Arrays.stream(dynamicSettings)
                .map(Supplier::get)
                .toArray();
    }
}

The idea is in the name: return new instance when values of the parameters relevant for its creation have changed. 这个想法的名称是:当与其创建相关的参数值发生更改时,返回新实例。 constructor is always a lambda wrapping a constructor - it can't return null. constructor始终是包装构造函数的lambda-它不能返回null。 It can throw an exception, though Used in our project like this: 它可能会引发异常,尽管在我们的项目中像这样使用:

new Refreshable<Service>(
    () -> new Service(settings.getParam1(), settings.getParam2()),
    settings::getParam1,
    settings::getParam1);

where parameter values returned by settings can change over time. settings返回的参数值可以随时间变化。

Long story short, under some load testing getInstance() returned null to several threads on first invocation (10 or more of them in the same second), at a line like this: serviceRefreshable.getInstance().someMethod() . 长话短说,在某些负载测试下, getInstance()在第一次调用时将空值返回给多个线程(同一秒中有10个或更多),在这样的一行: serviceRefreshable.getInstance().someMethod() So it's either the refreshable that was null, or the result of getInstance() and the fact that the error didn't repeat later in the run suggests the latter because refreshable is only assigned once (it's a singleton bean). 因此,要么是可刷新的为null,要么是getInstance()的结果,并且错误没有在稍后的运行中重复出现,这表明后者是因为可刷新只分配了一次(这是一个单例bean)。

I understand that the constructor can throw an exception and prevent instance from being assigned, but it's not in the log (and NPE is). 我知道constructor可以引发异常并阻止分配instance ,但是它不在日志中(而NPE是)。

Arrays.equals() here can't return true unexpectedly, because instanceKey is null initially and getCurrentKey() can't return null. 此处的Arrays.equals()不会意外返回true,因为instanceKey最初为null, getCurrentKey()不能返回null。 All dynamicSettings suppliers are lambdas wrapping this invocation: 所有dynamicSettings供应商都是包装此调用的lambda:

private <T> T getValue(String key, Class<T> type) {
    Object value = properties.get(key);
    if (value == null) {
        throw new ConfigurationException("No such setting: " + key);
    }
    if (!type.isInstance(value)) {
        throw new ConfigurationException("Wrong setting type" + key);
    }
    return (T) value;
}

How is this possible? 这怎么可能?

I also understand one can safeguard against this by wrapping instance and instanceKey in one object and reducing 2 assignments to one to make them atomic, as well as checking for instance == null along with comparing arrays. 我也知道,可以通过将instance和instanceKey封装在一个对象中并减少2个分配以使其成为原子,以及检查instance == null以及比较数组来防止这种情况。 What I don't understand is where the hell did the exception that caused constructor to fail go :) 我不明白的是,导致constructor失败的异常发生在哪里:)

I even edited the production code to throw a RuntimeException in a constructor lambda supplied to one of these Refreshables and sure enough, I saw that exception in the log instead of a NPE. 我什至还编辑了生产代码,以在提供给这些Refreshables之一的constructor lambda中抛出RuntimeException,而且确实,我在日志中看到了该异常,而不是NPE。 Wasn't under any kind of concurrent load, though 虽然没有任何并发​​负载

You don't have sufficient synchronization to guarantee that a thread that sees an updated value of instanceKey also sees the corresponding value of instance . 您没有足够的同步来确保看到instanceKey更新值的线程也看到相应的instance值。 So, a calling thread can bypass the synchronized block, and return instance before it is assigned the first time. 因此,调用线程可以绕过synchronized块,并在第一次分配它之前返回instance

Thread 1                          Thread 2
-------------------------         ----------------------------
instanceKey = currentKey;
                                  if (!Arrays.equals(currentKey, instanceKey)) {} // false
                                  return instance;
instance = constructor.get();

You are assuming that the assignment of instanceKey and instance to appear to be atomic to all other threads. 您假设对所有其他线程来说, instanceKeyinstance的分配似乎是原子的。 But that won't happen unless all other threads read those variables while synchronized on the same lock. 除非所有其他线程在同一锁上同步时读取这些变量,否则不会发生这种情况。

Long story short, under some load testing getInstance() returned null to several threads on first invocation 长话短说,在某些负载测试下,getInstance()在首次调用时向多个线程返回了null

As I mentioned in my comment, there is a race condition where instanceKey has been assigned inside the synchronized block but instance has not yet been assigned and may be null. 正如我在评论中提到的那样,有一个竞争条件,其中instanceKey已在同步块分配,但instance尚未分配,并且可能为null。 So after the one thread assigns the key, other threads will test the key, find it not null and equals and continue to return a null instance . 因此,在一个线程分配了密钥之后,其他线程将测试该密钥,发现它不为null并等于并且继续返回null instance

I think the creation of a class which holds both values is in order. 我认为创建同时包含这两个值的类是有序的。 I initially implemented this inside of an AtomicReference but I think just using volatile will get the job done. 我最初在AtomicReference内部实现了此功能,但我认为仅使用volatile完成工作。 Something like: 就像是:

private volatile InstanceInfo<T> instanceInfo;
...
public T getInstance() {
    // we store this in a non-volatile to only touch the volatile field once and to
    // ensure we have a consistent view of the instanceInfo value
    InstanceInfo<T> localInstanceInfo = instanceInfo;
    if (localInstanceInfo == null || !Arrays.equals(currentKey, localInstanceInfo.key)) {
       localInstanceInfo = new InstanceInfo<>(currentKey, constructor.get());
       instanceInfo = localInstanceInfo;
    }
    return localInstanceInfo.instance;
}
...
private static class InstanceInfo<T> {
   final Object[] key;
   final T instance; 
   public InstanceInfo(Object[] key, T instance) {
       this.key = key;
       this.instance = instance;
   }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM