在我的應用程式中,我希望有一個快取,該快取將通過昂貴的操作每天更新(例如,從遠程獲取并在本地計算)。這個想法是每天,它將獲取并計算今天的最新快取。在今天的任何時候,任何執行緒都應該能夠讀取/寫入每日快取。當昂貴的操作每天運行時提供舊資料是可以的,并且不應該在任何時候阻止任何請求。
我撰寫了一個簡單的代碼來說明這個想法,但不確定它是否是最佳實踐,甚至在多執行緒方面是否正確。例如,
- 是
volatile必需的嗎? - 如果在 get 或 put 中間完成快取重新分配會發生什么。
任何建議將不勝感激!
public class DailyCache {
private volatile ConcurrentHashMap<String, String> cache;
public DailyCache() {
cache = expensiveCalculation();
Executors.newScheduledThreadPool(1)
.scheduleAtFixedRate(() -> cache = expensiveCalculation(), 1, 1, TimeUnit.DAYS);
}
public String get(String key) {
return cache.get(key);
}
public void put(String key, String value) {
cache.put(key, value);
}
public ConcurrentHashMap<String, String> expensiveCalculation() {
// an expensive operation to fetch the cache for today
}
}
uj5u.com熱心網友回復:
執行服務
首先,您需要捕獲從 call 回傳的參考Executors.newScheduledThreadPool(1)。您必須將該參考存盤在您的應用程式中的某個位置,以便最終關閉。如果你忽略了關閉一個執行器服務,它的后臺執行緒池可能會在你的應用退出后繼續運行,就像一個僵尸???♂?。
順便說一下,對于單執行緒執行器服務,呼叫便捷方法Executors.newSingleThreadExecutor。
替換條目
如果您使用的是執行緒安全集合,那么我會在更新期間替換單個元素。您是否需要同時替換所有地圖條目?如果是這樣,請在您的問題中這么說。
如果您沒有理由更換整個地圖,請更改您的代碼以將您的標記cache為final. 如果您在第一次嘗試訪問它之前就確定了地圖的存在,并且您從不替換它,則無需將其標記為volatile.
另一件事:通常最好使您的欄位成為最適合的型別。所以在這種情況下,ConcurrentMap而不是將自己鎖定在ConcurrentHashMap.
public class DailyCache {
private final ConcurrentMap< String, String > cache;
private final ScheduledExecutorService ses;
public DailyCache() {
this.cache = new ConcurrentHashMap<>() ;
this.expensiveCalculation();
this.ses = Executors.newSingleThreadExecutor() ;
this.ses.scheduleAtFixedRate(() -> cache = expensiveCalculation(), 1, 1, TimeUnit.DAYS );
}
public String get( String key ) {
return this.cache.get( key );
}
public void put( String key, String value) {
this.cache.put( key, value );
}
public void expensiveCalculation() {
// A series of expensive operations to replace each element/entry of the `Map` cache.
}
public void shutdown() {
// Gracefully shut down the scheduled executor service held in var `ses`.
…
}
}
這種入口替換方法的一個優點是新資料可以更快地到達用戶手中,呼叫代碼可以使用早期的替換,而后面的替換尚未完成。
更換地圖
volatile
如果您有理由一次全部替換整個映射,而不是替換單個條目,那么您應該將cache宣告設為非final, 和volatile。
public class DailyCache {
private volatile ConcurrentMap< String, String > cache;
…
但我更喜歡不同的方法。由于具體定義的變化,以及一般并發的復雜性,我預計volatile關鍵字并不是所有程式員都清楚地理解。
AtomicReference
所以我會使用一個AtomicReference物件來保存對當前地圖物件的參考。看到那個AtomicReference宣告應該會讓你的意圖對另一個程式員很明顯。
AnAtomicReference是物件參考的執行緒安全持有者。
- var ? reference ? object
With a conventional reference, such as thecachevariable seen above, we are one step away from the map object: Thecachevariable holds a reference which at runtime takes us to theConcurrentMapobject floating within the heap somewhere. - var ? reference ? object ? reference ? object
With anAtomicReference, we are two steps away from the desired object. Thecachevariable seen below is a reference to an object whose content is the reference to yet another object.
AnAtomicReference一開始想起來有點奇怪,因為在 Java 中我們認為像cache上面這樣的變數是物件,即使我們知道它離物件只有一步之遙。相比之下,anAtomicReference使我們非常清楚我們正在操作對包含對物件的參考的物件的參考。例如,注意下面的.get().get(…)and.get().put(…)呼叫。
public class DailyCache {
private final AtomicReference < ConcurrentMap< String, String > > cache;
…
這是完整的課程,為AtomicReference.
public class DailyCache {
private final AtomicReference < ConcurrentMap< String, String > > cache;
private final ScheduledExecutorService ses;
public DailyCache() {
this.cache = new AtomicReference<>( new ConcurrentHashMap< String, String > () ) ;
this.expensiveCalculation() ;
this.ses = Executors.newSingleThreadExecutor() ;
this.ses.scheduleAtFixedRate(() -> cache = expensiveCalculation(), 1, 1, TimeUnit.DAYS );
}
public String get( String key ) {
return this.cache.get().get( key );
}
public void put( String key, String value) {
this.cache.get().put( key, value );
}
public void expensiveCalculation() {
ConcurrentMap< String, String > concurrentMap = … // An expensive operation to produce a new `ConcurrentMap`.
this.cache.set( concurrentMap ) ;
}
public void shutdown() {
// Gracefully shut down the scheduled executor service held in var `ses`.
…
}
}
uj5u.com熱心網友回復:
要回答您的問題:
問:這是“最佳實踐”嗎
沒有最佳實踐。如果您還沒有這樣做,請花時間閱讀。
這個問題無法回答……甚至沒有意義。
提示:是時候從你的詞匯表中洗掉“最佳實踐”了......并開始質疑那些告訴你某事是“最佳實踐”的人的智慧。
問:
volatile需要嗎?
或許。這取決于是否可以cache更改參考。
- 如果可能,那么
volatile是必需的。 - 如果不可能,則
volatile可能不需要。但在這種情況下,您應該將變數宣告為final. 如果你這樣做,那么 JLS 保證所有執行緒都會看到正確的變數值。
在您的代碼中,您似乎正在定期為cache. 如果是這樣,那么它需要是volatile,或者您需要其他方式來確保所有作業執行緒都看到更新的cache值。(還有其他方法......但這開始有過早優化的味道。)
問:如果在 get 或 put 程序中完成了快取重新分配,會發生什么情況。
如果getorput呼叫在賦值之前開始,那么它們肯定會在舊快取上進行操作。如果不是,則不是,這將取決于對 fetch 是cache發生在分配之前還是之后。這是不可預測的。
還有一件事你似乎沒有考慮過。你說:
當昂貴的操作每天運行時提供舊資料是可以的,并且不應該在任何時候阻止任何請求。
但是您還沒有說在構建新快取時更新舊快取是否可以(或不可以)。例如,考慮以下事件序列:
- 開始重建快取
- 快取重建器更新新快取中的 key1 -> value1
- 主應用程式讀取并更新舊快取中的 key1 -> value2
- 快取被替換。
現在我們使用新快取運行應用程式,但新快取包含 key1 的 value1,它比最近使用的 value (value2) 更舊。
如果這破壞了您的應用程式,那么您需要一種方法來解決它。會有一些方法......但這將取決于您沒有提到的應用程式邏輯的各個方面。例如,主應用程式將執行put操作的場景。
轉載請註明出處,本文鏈接:https://www.uj5u.com/gongcheng/412302.html
標籤:
