同步块中的易失性变量赋值导致 null

Volatile variable assignment in a synchronized block results in null

这里是 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();
    }
}

这个想法在名称中:return 当与其创建相关的参数值发生变化时的新实例。 constructor 始终是包装构造函数的 lambda - 它不能 return null。它可以抛出异常,但 在我们的项目中这样使用:

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

其中由 settings 编辑的参数值 return 可以随时间变化。

长话短说,在某些负载测试下 getInstance() return在第一次调用时将 null 设置为多个线程(同一秒内有 10 个或更多线程),像这样一行:serviceRefreshable.getInstance().someMethod()。所以它要么是空的可刷新的,要么是 getInstance() 的结果,而且错误没有在 运行 中重复的事实表明是后者,因为可刷新的只分配一次(它是一个单例 bean)。

我知道 constructor 可以抛出异常并阻止 instance 被分配,但它不在日志中(NPE 是)。

Arrays.equals()这里竟然不能return真,因为instanceKey最初是null,而getCurrentKey()不能returnnull。所有 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;
}

这怎么可能?

我也知道可以通过将 instance 和 instanceKey 包装在一个对象中并将 2 个赋值减少为一个以使其成为原子,以及检查 instance == null 以及比较数组来防止这种情况。 我不明白的是导致 constructor 失败的异常到底去了哪里:)

我什至编辑了生产代码以在提供给其中一个 Refreshablesconstructor lambda 中抛出 RuntimeException,果然,我在日志中看到了该异常而不是 NPE。虽然没有任何类型的并发负载

您没有足够的同步来保证看到 instanceKey 更新值的线程也看到 instance 的对应值。因此,调用线程可以绕过 synchronized 块,并且 return instance 在它第一次被分配之前。

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

您假设 instanceKeyinstance 的分配对所有其他线程来说都是原子的。但这不会发生,除非所有其他线程在同一个锁上同步时读取这些变量。

Long story short, under some load testing getInstance() returned null to several threads on first invocation

正如我在评论中提到的,存在一种竞争条件,其中 instanceKey 已分配 同步块内,但 instance 尚未分配已分配且可能为空。所以一个线程分配key后,其他线程会测试key,发现不为null且等于继续return一个nullinstance.

我认为创建一个包含两个值的 class 是合适的。我最初在 AtomicReference 中实现了这个,但我认为只使用 volatile 就可以完成工作。类似于:

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