同步块中的易失性变量赋值导致 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
失败的异常到底去了哪里:)
我什至编辑了生产代码以在提供给其中一个 Refreshables
的 constructor
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();
您假设 instanceKey
和 instance
的分配对所有其他线程来说都是原子的。但这不会发生,除非所有其他线程在同一个锁上同步时读取这些变量。
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;
}
}
这里是 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
失败的异常到底去了哪里:)
我什至编辑了生产代码以在提供给其中一个 Refreshables
的 constructor
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();
您假设 instanceKey
和 instance
的分配对所有其他线程来说都是原子的。但这不会发生,除非所有其他线程在同一个锁上同步时读取这些变量。
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;
}
}