我构建了一个通用缓存,通过委托给ValueGenerator
组件来获取未命中的值。在内部,它有一个已经获得的值的映射,以及另一个用于飞行中请求的映射,以便它们可以在订阅者之间重复使用。
这是我尝试使其线程安全之前的简化代码。我的问题将随之而来。
public class NetworkCache<K, V> {
private final Map<K, V> mValues;
private final Map<K, Observable<V>> mRequests;
private final ValueGenerator<K, V> mValueGenerator;
public NetworkCache(ValueGenerator<K, V> valueGenerator) {
mValues = new HashMap<>();
mRequests = new HashMap<>();
mValueGenerator = valueGenerator;
}
public Observable<V> get(final K key) {
V value = mValues.get(key);
if (value != null) {
// We already had the value
return Observable.just(value);
}
Observable<V> requestObservable = mRequests.get(key);
if (requestObservable == null) {
// New request to fetch the value
requestObservable = mValueGenerator.generate(key);
// Store in-flight request for potential re-use
mRequests.put(key, requestObservable);
requestObservable.subscribe(new Subscriber<V>() {
@Override
public void onCompleted() { mRequests.remove(key); }
@Override
public void onError(Throwable e) { mRequests.remove(key); }
@Override
public void onNext(V value) { mValues.put(key, value); }
});
}
return requestObservable;
}
public interface ValueGenerator<K, V> {
Observable<V> generate(K key);
}
}
现在,我正在考虑如何在并发场景下破解这种情况。我认为重点应放在Map
中查询并在get()
回调中进行修改的两个subscribe
。
我认为假设/强制执行此类只能在主线程上调用是合理的。但是,ValueGenerator
应该能够在不同的线程上安排其工作,因为我的用例实际上是网络请求。
我看到3个选项,我希望帮助找出使用哪个选项。
ConcurrentHashMap
代替HashMap
构造函数将更改为:
public NetworkCache(ValueGenerator<K, V> valueGenerator) {
mValues = new ConcurrentHashMap<>();
mRequests = new ConcurrentHashMap<>();
mValueGenerator = valueGenerator;
}
通过这种方法,我不知道它是否足够和/或是否过度。
ValueGenerator
调用对我来说,这意味着所有地图操作都会在主线程上发生(假设NetworkCache
仅在那里使用),即使ValueGenerator
使用了subscribeOn(Schedulers.io())
。这意味着它是线程安全的。
if (requestObservable == null) {
// New request to fetch the value
requestObservable = mValueGenerator
.generate(key)
.observeOn(AndroidSchedulers.mainThread());
...
我将继续使用HashMap
,get
方法将成为以下方法。在这里,正确的方法是在地图本身上同步吗?我是否需要阻止每次操作,或只是put
&amp; remove
?
public Observable<V> get(final K key) {
V value;
synchronized (mValues) {
value = mValues.get(key);
}
if (value != null) {
return Observable.just(value);
}
Observable<V> requestObservable;
synchronized (mRequests) {
requestObservable = mRequests.get(key);
}
if (requestObservable == null) {
requestObservable = mValueGenerator.generate(key);
synchronized (mRequests) {
mRequests.put(key, requestObservable);
}
requestObservable.subscribe(new Subscriber<V>() {
@Override
public void onCompleted() {
synchronized (mRequests) {
mRequests.remove(key);
}
}
@Override
public void onError(Throwable e) {
synchronized (mRequests) {
mRequests.remove(key);
}
}
@Override
public void onNext(V value) {
synchronized (mValues) {
mValues.put(key, value);
}
}
});
}
return requestObservable;
}
关于利用率的一些背景知识:对于不同的密钥,缓存的get
方法将被快速连续1-10次调用。那个事件很少发生,但可能会在几秒钟内发生。当第二系列的呼叫到来时,与第一系列的观察结果混合在一起,我担心执行。
答案 0 :(得分:1)
我会使用一个ConcurrentMap
和AsyncSubject
:
public class RequestCache<K, V> {
final ConcurrentMap<K, AsyncSubject<V>> values;
final Function<? super K, ? extends Observable<? extends V>> valueGenerator;
public RequestCache(
Function<? super K, ? extends Observable<? extends V>> valueGenerator) {
this.values = new ConcurrentHashMap<>();
this.valueGenerator = valueGenerator;
}
public Observable<V> get(K key) {
AsyncSubject<V> result = values.get(key);
if (result == null) {
result = AsyncSubject.create();
AsyncSubject<V> current = values.putIfAbsent(key, result);
if (current == null) {
Observable<? extends V> source = valueGenerator.apply(key);
source.subscribe(result);
} else {
result = current;
}
}
return result;
}
}
这完全是线程安全的,并且每个键仅调用valueGenerator
一次。
答案 1 :(得分:0)
我认为你应该同步整个getter和setter函数。