簡體   English   中英

java中一個對象的線程安全緩存

[英]Thread-safe cache of one object in java

假設我們的應用程序中有一個CountryList對象,它應返回國家/地區列表。 加載國家是一項繁重的操作,因此應該緩存該列表。

其他要求:

  • CountryList應該是線程安全的
  • CountryList應加載延遲(僅按需)
  • CountryList應支持緩存失效
  • 考慮到緩存很少會失效,應優化CountryList

我提出了以下解決方案:

public class CountryList {
    private static final Object ONE = new Integer(1);

    // MapMaker is from Google Collections Library    
    private Map<Object, List<String>> cache = new MapMaker()
        .initialCapacity(1)
        .makeComputingMap(
            new Function<Object, List<String>>() {
                @Override
                public List<String> apply(Object from) {
                    return loadCountryList();
                }
            });

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        return cache.get(ONE);
    }

    public void invalidateCache() {
        cache.remove(ONE);
    }
}

你怎么看待這件事? 你覺得它有什么壞處嗎? 還有其他辦法嗎? 我怎樣才能讓它變得更好? 我應該在這種情況下尋找另一種解決方案嗎?

謝謝。

谷歌收藏實際上只提供這類東西: 供應商

你的代碼是這樣的:

private Supplier<List<String>> supplier = new Supplier<List<String>>(){
    public List<String> get(){
        return loadCountryList();
    }
};


// volatile reference so that changes are published correctly see invalidate()
private volatile Supplier<List<String>> memorized = Suppliers.memoize(supplier);


public List<String> list(){
    return memorized.get();
}

public void invalidate(){
    memorized = Suppliers.memoize(supplier);
}

謝謝你們所有人 ,尤其是那些提出這個想法的用戶“ gid ”。

我的目標是優化get()操作的性能,因為invalidate()操作將被稱為非常罕見。

我寫了一個測試類,它啟動了16個線程,每個線程調用get() - 操作一百萬次。 通過這個課程,我在我的2核機器上描述了一些實現。

測試結果

Implementation              Time
no synchronisation          0,6 sec
normal synchronisation      7,5 sec
with MapMaker               26,3 sec
with Suppliers.memoize      8,2 sec
with optimized memoize      1,5 sec

1)“無同步”不是線程安全的,但為我們提供了可以比較的最佳性能。

@Override
public List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public void invalidateCache() {
    cache = null;
}

2)“正常同步” - 相當不錯的性能,標准的無需實施

@Override
public synchronized List<String> list() {
    if (cache == null) {
        cache = loadCountryList();
    }
    return cache;
}

@Override
public synchronized void invalidateCache() {
    cache = null;
}

3)“與MapMaker” - 性能非常差。

請在頂部查看我的問題代碼。

4)“with Suppliers.memoize” - 良好的表現。 但由於性能相同“正常同步”,我們需要對其進行優化或僅使用“正常同步”。

有關代碼,請參閱用戶“gid”的答案。

5) “具有優化的memoize” - 性能與“無同步 - 實現相當,但是線程安全的。 這是我們需要的。

緩存類本身:(此處使用的供應商界面來自Google Collections Library,它只有一個方法get()。請參閱http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/ common / base / Supplier.html

public class LazyCache<T> implements Supplier<T> {
    private final Supplier<T> supplier;

    private volatile Supplier<T> cache;

    public LazyCache(Supplier<T> supplier) {
        this.supplier = supplier;
        reset();
    }

    private void reset() {
        cache = new MemoizingSupplier<T>(supplier);
    }

    @Override
    public T get() {
        return cache.get();
    }

    public void invalidate() {
        reset();
    }

    private static class MemoizingSupplier<T> implements Supplier<T> {
        final Supplier<T> delegate;
        volatile T value;

        MemoizingSupplier(Supplier<T> delegate) {
            this.delegate = delegate;
        }

        @Override
        public T get() {
            if (value == null) {
                synchronized (this) {
                    if (value == null) {
                        value = delegate.get();
                    }
                }
            }
            return value;
        }
    }
}

使用示例:

public class BetterMemoizeCountryList implements ICountryList {

    LazyCache<List<String>> cache = new LazyCache<List<String>>(new Supplier<List<String>>(){
        @Override
        public List<String> get() {
            return loadCountryList();
        }
    });

    @Override
    public List<String> list(){
        return cache.get();
    }

    @Override
    public void invalidateCache(){
        cache.invalidate();
    }

    private List<String> loadCountryList() {
        // this should normally load a full list from the database,
        // but just for this instance we mock it with:
        return Arrays.asList("Germany", "Russia", "China");
    }
}

每當我需要緩存某些東西時,我都喜歡使用代理模式 使用這種模式可以解決問題。 您的原始對象可能與延遲加載有關。 您的代理(或監護人)對象可以負責驗證緩存。

詳細地:

  • 定義一個對象的CountryList類,它是線程安全的,最好使用同步塊或其他信號量鎖。
  • 將此類的接口解壓縮到CountryQueryable接口。
  • 定義另一個實現CountryQueryable的對象CountryListProxy。
  • 僅允許實例化CountryListProxy,並且僅允許通過其接口引用它。

從這里,您可以將緩存失效策略插入代理對象。 保存上次加載的時間,並在下次查看數據的請求時,將當前時間與緩存時間進行比較。 定義容差級別,如果時間過長,則重新加載數據。

至於Lazy Load,請參閱此處

現在為一些好的家庭示例代碼:

public interface CountryQueryable {

    public void operationA();
    public String operationB();

}

public class CountryList implements CountryQueryable {

    private boolean loaded;

    public CountryList() {
        loaded = false;
    }

    //This particular operation might be able to function without
    //the extra loading.
    @Override
    public void operationA() {
        //Do whatever.
    }

    //This operation may need to load the extra stuff.
    @Override
    public String operationB() {
        if (!loaded) {
            load();
            loaded = true;
        }

        //Do whatever.
        return whatever;
    }

    private void load() {
        //Do the loading of the Lazy load here.
    }

}

public class CountryListProxy implements CountryQueryable {

    //In accordance with the Proxy pattern, we hide the target
    //instance inside of our Proxy instance.
    private CountryQueryable actualList;
    //Keep track of the lazy time we cached.
    private long lastCached;

    //Define a tolerance time, 2000 milliseconds, before refreshing
    //the cache.
    private static final long TOLERANCE = 2000L;

    public CountryListProxy() {
            //You might even retrieve this object from a Registry.
        actualList = new CountryList();
        //Initialize it to something stupid.
        lastCached = Long.MIN_VALUE;
    }

    @Override
    public synchronized void operationA() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }
    }

    @Override
    public synchronized String operationB() {
        if ((System.getCurrentTimeMillis() - lastCached) > TOLERANCE) {
            //Refresh the cache.
                    lastCached = System.getCurrentTimeMillis();
        } else {
            //Cache is okay.
        }

        return whatever;
    }

}

public class Client {

    public static void main(String[] args) {
        CountryQueryable queryable = new CountryListProxy();
        //Do your thing.
    }

}

我不確定地圖的用途。 當我需要一個懶惰的緩存對象時,我通常會這樣做:

public class CountryList
{
  private static List<Country> countryList;

  public static synchronized List<Country> get()
  {
    if (countryList==null)
      countryList=load();
    return countryList;
  }
  private static List<Country> load()
  {
    ... whatever ...
  }
  public static synchronized void forget()
  {
    countryList=null;
  }
}

我認為這與你正在做的相似,但有點簡單。 如果您需要地圖以及您為問題簡化過的那個,那么好吧。

如果你想要它是線程安全的,你應該同步get和forget。

你怎么看待這件事? 你覺得它有什么壞處嗎?

Bleah - 您正在使用復雜的數據結構MapMaker,它具有多個功能(映射訪問,並發友好訪問,值延遲構造等),因為您正在使用的單個功能(延遲創建單個構造昂貴的對象) 。

雖然重用代碼是一個很好的目標,但這種方法增加了額外的開銷和復雜性。 此外,當他們看到地圖數據結構時會誤導未來的維護者,以為當那里只有一件事(國家列表)時,會有一個鍵/值的映射。 簡單性,可讀性和清晰度是未來可維護性的關鍵。

還有其他辦法嗎? 我怎樣才能讓它變得更好? 我應該在這種情況下尋找另一種解決方案嗎?

好像你是在懶惰加載后。 看看其他SO延遲加載問題的解決方案。 例如,這個涵蓋了經典的雙重檢查方法(確保您使用的是Java 1.5或更高版本):

如何解決Java中的“雙重檢查已破壞”聲明?

我不認為只是簡單地在這里重復解決方案代碼,而是通過仔細閱讀有關延遲加載的討論來擴展您的知識庫。 (對不起,如果那是浮誇的 - 只是嘗試教魚而不是喂等等等等......)

那里有一個庫(來自atlassian ) - 一個名為LazyReference的util類。 LazyReference是對可以延遲創建的對象的引用(在第一次獲取時)。 它是guarenteed線程安全的,並且init也被保證只發生一次 - 如果兩個線程同時調用get(),一個線程將計算,另一個線程將阻塞等待。

看一個示例代碼

final LazyReference<MyObject> ref = new LazyReference() {
    protected MyObject create() throws Exception {
        // Do some useful object construction here
        return new MyObject();
    }
};

//thread1
MyObject myObject = ref.get();
//thread2
MyObject myObject = ref.get();

這里你的需求看起來很簡單。 MapMaker的使用使得實現變得更加復雜。 整個雙重檢查鎖定成語很難做到正確,只適用於1.5+。 說實話,它打破了最重要的編程規則之一:

過早優化是萬惡之源。

雙重檢查的鎖定習慣用法試圖在已經加載高速緩存的情況下避免同步的成本。 但這個開銷真的會引起問題嗎? 更復雜的代碼是否值得? 我說假設它不會直到分析告訴你。

這是一個非常簡單的解決方案,不需要第三方代碼(忽略JCIP注釋)。 它確實假設空列表意味着尚未加載緩存。 它還可以防止國家/地區列表的內容轉義為可能修改返回列表的客戶端代碼。 如果您不關心這一點,可以刪除對Collections.unmodifiedList()的調用。

public class CountryList {

    @GuardedBy("cache")
    private final List<String> cache = new ArrayList<String>();

    private List<String> loadCountryList() {
        // HEAVY OPERATION TO LOAD DATA
    }

    public List<String> list() {
        synchronized (cache) {
            if( cache.isEmpty() ) {
                cache.addAll(loadCountryList());
            }
            return Collections.unmodifiableList(cache);
        }
    }

    public void invalidateCache() {
        synchronized (cache) {
            cache.clear();
        }
    }

}

這對我來說沒什么問題(我假設MapMaker來自Google收藏?)理想情況下你不需要使用Map,因為你沒有真正擁有密鑰但是因為實現對任何調用者都是隱藏的我不認為這是一個很重要。

這是使用ComputingMap的簡單方法。 你只需要一個簡單的實現,所有方法都是同步的,你應該沒問題。 這顯然會阻止第一個線程命中它(獲取它),以及任何其他線程在第一個線程加載緩存時命中它(如果有人調用invalidateCache事件,則再次相同 - 你還應該決定invalidateCache是​​否應該加載重新緩存,或者只是將其清空,讓第一次嘗試再次阻止它),但是所有線程都應該很好地完成。

使用Initialization on demand holder慣用法

public class CountryList {
  private CountryList() {}

  private static class CountryListHolder {
    static final List<Country> INSTANCE = new List<Country>();
  }

  public static List<Country> getInstance() {
    return CountryListHolder.INSTANCE;
  }

  ...
}

跟進Mike的解決方案。 我的評論沒有按預期格式...... :(

注意operationB中的同步問題,特別是因為load()很慢:

public String operationB() {
    if (!loaded) {
        load();
        loaded = true;
    }

    //Do whatever.
    return whatever;
}

你可以這樣解決它:

public String operationB() {
    synchronized(loaded) {
        if (!loaded) {
            load();
            loaded = true;
        }
    }

    //Do whatever.
    return whatever;
}

確保在每次訪問加載的變量時始終保持同步。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM