国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

動手實現一個 LRU cache

Cc_2011 / 1783人閱讀

摘要:不過其中的流程算是一個簡易的實現,可以對加深一些理解。實現二因此如何來實現一個完整的緩存呢,這次不考慮過期時間的問題。緩存數量超過閾值時移除鏈表尾部數據。

前言

LRU 是 Least Recently Used 的簡寫,字面意思則是最近最少使用。

通常用于緩存的淘汰策略實現,由于緩存的內存非常寶貴,所以需要根據某種規則來剔除數據保證內存不被撐滿。

如常用的 Redis 就有以下幾種策略:

策略 描述
volatile-lru 從已設置過期時間的數據集中挑選最近最少使用的數據淘汰
volatile-ttl 從已設置過期時間的數據集中挑選將要過期的數據淘汰
volatile-random 從已設置過期時間的數據集中任意選擇數據淘汰
allkeys-lru 從所有數據集中挑選最近最少使用的數據淘汰
allkeys-random 從所有數據集中任意選擇數據進行淘汰
no-envicition 禁止驅逐數據
摘抄自:https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Redis.md#%E5%8D%81%E4%B8%89%E6%95%B0%E6%8D%AE%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5
實現一

之前也有接觸過一道面試題,大概需求是:

實現一個 LRU 緩存,當緩存數據達到 N 之后需要淘汰掉最近最少使用的數據。

N 小時之內沒有被訪問的數據也需要淘汰掉。

以下是我的實現:

public class LRUAbstractMap extends java.util.AbstractMap {

    private final static Logger LOGGER = LoggerFactory.getLogger(LRUAbstractMap.class);

    /**
     * 檢查是否超期線程
     */
    private ExecutorService checkTimePool ;

    /**
     * map 最大size
     */
    private final static int MAX_SIZE = 1024 ;

    private final static ArrayBlockingQueue QUEUE = new ArrayBlockingQueue<>(MAX_SIZE) ;

    /**
     * 默認大小
     */
    private final static int DEFAULT_ARRAY_SIZE =1024 ;


    /**
     * 數組長度
     */
    private int arraySize ;

    /**
     * 數組
     */
    private Object[] arrays ;


    /**
     * 判斷是否停止 flag
     */
    private volatile boolean flag = true ;


    /**
     * 超時時間
     */
    private final static Long EXPIRE_TIME = 60 * 60 * 1000L ;

    /**
     * 整個 Map 的大小
     */
    private volatile AtomicInteger size  ;


    public LRUAbstractMap() {


        arraySize = DEFAULT_ARRAY_SIZE;
        arrays = new Object[arraySize] ;

        //開啟一個線程檢查最先放入隊列的值是否超期
        executeCheckTime();
    }

    /**
     * 開啟一個線程檢查最先放入隊列的值是否超期 設置為守護線程
     */
    private void executeCheckTime() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("check-thread-%d")
                .setDaemon(true)
                .build();
        checkTimePool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(1),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
        checkTimePool.execute(new CheckTimeThread()) ;

    }

    @Override
    public Set entrySet() {
        return super.keySet();
    }

    @Override
    public Object put(Object key, Object value) {
        int hash = hash(key);
        int index = hash % arraySize ;
        Node currentNode = (Node) arrays[index] ;

        if (currentNode == null){
            arrays[index] = new Node(null,null, key, value);

            //寫入隊列
            QUEUE.offer((Node) arrays[index]) ;

            sizeUp();
        }else {
            Node cNode = currentNode ;
            Node nNode = cNode ;

            //存在就覆蓋
            if (nNode.key == key){
                cNode.val = value ;
            }

            while (nNode.next != null){
                //key 存在 就覆蓋 簡單判斷
                if (nNode.key == key){
                    nNode.val = value ;
                    break ;
                }else {
                    //不存在就新增鏈表
                    sizeUp();
                    Node node = new Node(nNode,null,key,value) ;

                    //寫入隊列
                    QUEUE.offer(currentNode) ;

                    cNode.next = node ;
                }

                nNode = nNode.next ;
            }

        }

        return null ;
    }


    @Override
    public Object get(Object key) {

        int hash = hash(key) ;
        int index = hash % arraySize ;
        Node currentNode = (Node) arrays[index] ;

        if (currentNode == null){
            return null ;
        }
        if (currentNode.next == null){

            //更新時間
            currentNode.setUpdateTime(System.currentTimeMillis());

            //沒有沖突
            return currentNode ;

        }

        Node nNode = currentNode ;
        while (nNode.next != null){

            if (nNode.key == key){

                //更新時間
                currentNode.setUpdateTime(System.currentTimeMillis());

                return nNode ;
            }

            nNode = nNode.next ;
        }

        return super.get(key);
    }


    @Override
    public Object remove(Object key) {

        int hash = hash(key) ;
        int index = hash % arraySize ;
        Node currentNode = (Node) arrays[index] ;

        if (currentNode == null){
            return null ;
        }

        if (currentNode.key == key){
            sizeDown();
            arrays[index] = null ;

            //移除隊列
            QUEUE.poll();
            return currentNode ;
        }

        Node nNode = currentNode ;
        while (nNode.next != null){

            if (nNode.key == key){
                sizeDown();
                //在鏈表中找到了 把上一個節點的 next 指向當前節點的下一個節點
                nNode.pre.next = nNode.next ;
                nNode = null ;

                //移除隊列
                QUEUE.poll();

                return nNode;
            }

            nNode = nNode.next ;
        }

        return super.remove(key);
    }

    /**
     * 增加size
     */
    private void sizeUp(){

        //在put值時候認為里邊已經有數據了
        flag = true ;

        if (size == null){
            size = new AtomicInteger() ;
        }
        int size = this.size.incrementAndGet();
        if (size >= MAX_SIZE) {
            //找到隊列頭的數據
            Node node = QUEUE.poll() ;
            if (node == null){
                throw new RuntimeException("data error") ;
            }

            //移除該 key
            Object key = node.key ;
            remove(key) ;
            lruCallback() ;
        }

    }

    /**
     * 數量減小
     */
    private void sizeDown(){

        if (QUEUE.size() == 0){
            flag = false ;
        }

        this.size.decrementAndGet() ;
    }

    @Override
    public int size() {
        return size.get() ;
    }

    /**
     * 鏈表
     */
    private class Node{
        private Node next ;
        private Node pre ;
        private Object key ;
        private Object val ;
        private Long updateTime ;

        public Node(Node pre,Node next, Object key, Object val) {
            this.pre = pre ;
            this.next = next;
            this.key = key;
            this.val = val;
            this.updateTime = System.currentTimeMillis() ;
        }

        public void setUpdateTime(Long updateTime) {
            this.updateTime = updateTime;
        }

        public Long getUpdateTime() {
            return updateTime;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "key=" + key +
                    ", val=" + val +
                    "}";
        }
    }


    /**
     * copy HashMap 的 hash 實現
     * @param key
     * @return
     */
    public int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    private void lruCallback(){
        LOGGER.debug("lruCallback");
    }


    private class CheckTimeThread implements Runnable{

        @Override
        public void run() {
            while (flag){
                try {
                    Node node = QUEUE.poll();
                    if (node == null){
                        continue ;
                    }
                    Long updateTime = node.getUpdateTime() ;

                    if ((updateTime - System.currentTimeMillis()) >= EXPIRE_TIME){
                        remove(node.key) ;
                    }
                } catch (Exception e) {
                    LOGGER.error("InterruptedException");
                }
            }
        }
    }

}

感興趣的朋友可以直接從:

https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUAbstractMap.java

下載代碼本地運行。

代碼看著比較多,其實實現的思路還是比較簡單:

采用了與 HashMap 一樣的保存數據方式,只是自己手動實現了一個簡易版。

內部采用了一個隊列來保存每次寫入的數據。

寫入的時候判斷緩存是否大于了閾值 N,如果滿足則根據隊列的 FIFO 特性將隊列頭的數據刪除。因為隊列頭的數據肯定是最先放進去的。

再開啟了一個守護線程用于判斷最先放進去的數據是否超期(因為就算超期也是最先放進去的數據最有可能滿足超期條件。)

設置為守護線程可以更好的表明其目的(最壞的情況下,如果是一個用戶線程最終有可能導致程序不能正常退出,因為該線程一直在運行,守護線程則不會有這個情況。)

以上代碼大體功能滿足了,但是有一個致命問題。

就是最近最少使用沒有滿足,刪除的數據都是最先放入的數據。

不過其中的 put get 流程算是一個簡易的 HashMap 實現,可以對 HashMap 加深一些理解。
實現二

因此如何來實現一個完整的 LRU 緩存呢,這次不考慮過期時間的問題。

其實從上一個實現也能想到一些思路:

要記錄最近最少使用,那至少需要一個有序的集合來保證寫入的順序。

在使用了數據之后能夠更新它的順序。

基于以上兩點很容易想到一個常用的數據結構:鏈表。

每次寫入數據時將數據放入鏈表頭結點。

使用數據時候將數據移動到頭結點

緩存數量超過閾值時移除鏈表尾部數據。

因此有了以下實現:

public class LRUMap {
    private final Map cacheMap = new HashMap<>();

    /**
     * 最大緩存大小
     */
    private int cacheSize;

    /**
     * 節點大小
     */
    private int nodeCount;


    /**
     * 頭結點
     */
    private Node header;

    /**
     * 尾結點
     */
    private Node tailer;

    public LRUMap(int cacheSize) {
        this.cacheSize = cacheSize;
        //頭結點的下一個結點為空
        header = new Node<>();
        header.next = null;

        //尾結點的上一個結點為空
        tailer = new Node<>();
        tailer.tail = null;

        //雙向鏈表 頭結點的上結點指向尾結點
        header.tail = tailer;

        //尾結點的下結點指向頭結點
        tailer.next = header;


    }

    public void put(K key, V value) {
        cacheMap.put(key, value);

        //雙向鏈表中添加結點
        addNode(key, value);
    }

    public V get(K key){

        Node node = getNode(key);

        //移動到頭結點
        moveToHead(node) ;

        return cacheMap.get(key);
    }

    private void moveToHead(Node node){

        //如果是最后的一個節點
        if (node.tail == null){
            node.next.tail = null ;
            tailer = node.next ;
            nodeCount -- ;
        }

        //如果是本來就是頭節點 不作處理
        if (node.next == null){
            return ;
        }

        //如果處于中間節點
        if (node.tail != null && node.next != null){
            //它的上一節點指向它的下一節點 也就刪除當前節點
            node.tail.next = node.next ;
            nodeCount -- ;
        }

        //最后在頭部增加當前節點
        //注意這里需要重新 new 一個對象,不然原本的node 還有著下面的引用,會造成內存溢出。
        node = new Node<>(node.getKey(),node.getValue()) ;
        addHead(node) ;

    }

    /**
     * 鏈表查詢 效率較低
     * @param key
     * @return
     */
    private Node getNode(K key){
        Node node = tailer ;
        while (node != null){

            if (node.getKey().equals(key)){
                return node ;
            }

            node = node.next ;
        }

        return null ;
    }


    /**
     * 寫入頭結點
     * @param key
     * @param value
     */
    private void addNode(K key, V value) {

        Node node = new Node<>(key, value);

        //容量滿了刪除最后一個
        if (cacheSize == nodeCount) {
            //刪除尾結點
            delTail();
        }

        //寫入頭結點
        addHead(node);

    }



    /**
     * 添加頭結點
     *
     * @param node
     */
    private void addHead(Node node) {

        //寫入頭結點
        header.next = node;
        node.tail = header;
        header = node;
        nodeCount++;

        //如果寫入的數據大于2個 就將初始化的頭尾結點刪除
        if (nodeCount == 2) {
            tailer.next.next.tail = null;
            tailer = tailer.next.next;
        }

    }    

    private void delTail() {
        //把尾結點從緩存中刪除
        cacheMap.remove(tailer.getKey());

        //刪除尾結點
        tailer.next.tail = null;
        tailer = tailer.next;

        nodeCount--;

    }

    private class Node {
        private K key;
        private V value;
        Node tail;
        Node next;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }

        public Node() {
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }

    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder() ;
        Node node = tailer ;
        while (node != null){
            sb.append(node.getKey()).append(":")
                    .append(node.getValue())
                    .append("-->") ;

            node = node.next ;
        }


        return sb.toString();
    }
}

源碼:
https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUMap.java

實際效果,寫入時:

    @Test
    public void put() throws Exception {
        LRUMap lruMap = new LRUMap(3) ;
        lruMap.put("1",1) ;
        lruMap.put("2",2) ;
        lruMap.put("3",3) ;

        System.out.println(lruMap.toString());

        lruMap.put("4",4) ;
        System.out.println(lruMap.toString());

        lruMap.put("5",5) ;
        System.out.println(lruMap.toString());
    }

//輸出:
1:1-->2:2-->3:3-->
2:2-->3:3-->4:4-->
3:3-->4:4-->5:5-->

使用時:

    @Test
    public void get() throws Exception {
        LRUMap lruMap = new LRUMap(3) ;
        lruMap.put("1",1) ;
        lruMap.put("2",2) ;
        lruMap.put("3",3) ;

        System.out.println(lruMap.toString());
        System.out.println("==============");

        Integer integer = lruMap.get("1");
        System.out.println(integer);
        System.out.println("==============");
        System.out.println(lruMap.toString());
    }
    
//輸出
1:1-->2:2-->3:3-->
==============
1
==============
2:2-->3:3-->1:1-->

實現思路和上文提到的一致,說下重點:

數據是直接利用 HashMap 來存放的。

內部使用了一個雙向鏈表來存放數據,所以有一個頭結點 header,以及尾結點 tailer。

每次寫入頭結點,刪除尾結點時都是依賴于 header tailer,如果看著比較懵建議自己實現一個鏈表熟悉下,或結合下文的對象關系圖一起理解。

使用數據移動到鏈表頭時,第一步是需要在雙向鏈表中找到該節點。這里就體現出鏈表的問題了。查找效率很低,最差需要 O(N)。之后依賴于當前節點進行移動。

在寫入頭結點時有判斷鏈表大小等于 2 時需要刪除初始化的頭尾結點。這是因為初始化時候生成了兩個雙向節點,沒有數據只是為了形成一個數據結構。當真實數據進來之后需要刪除以方便后續的操作(這點可以繼續優化)。

以上的所有操作都是線程不安全的,需要使用者自行控制。

下面是對象關系圖:

初始化時

寫入數據時
LRUMap lruMap = new LRUMap(3) ;
lruMap.put("1",1) ;

lruMap.put("2",2) ;

lruMap.put("3",3) ;

lruMap.put("4",4) ;

獲取數據時

數據和上文一樣:

Integer integer = lruMap.get("2");

通過以上幾張圖應該是很好理解數據是如何存放的了。

實現三

其實如果對 Java 的集合比較熟悉的話,會發現上文的結構和 LinkedHashMap 非常類似。

對此不太熟悉的朋友可以先了解下 LinkedHashMap 底層分析 。

所以我們完全可以借助于它來實現:

public class LRULinkedMap {


    /**
     * 最大緩存大小
     */
    private int cacheSize;

    private LinkedHashMap cacheMap ;


    public LRULinkedMap(int cacheSize) {
        this.cacheSize = cacheSize;

        cacheMap = new LinkedHashMap(16,0.75F,true){
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                if (cacheSize + 1 == cacheMap.size()){
                    return true ;
                }else {
                    return false ;
                }
            }
        };
    }

    public void put(K key,V value){
        cacheMap.put(key,value) ;
    }

    public V get(K key){
        return cacheMap.get(key) ;
    }


    public Collection> getAll() {
        return new ArrayList>(cacheMap.entrySet());
    }
}

源碼:
https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRULinkedMap.java

這次就比較簡潔了,也就幾行代碼(具體的邏輯 LinkedHashMap 已經幫我們實現好了)

實際效果:

    @Test
    public void put() throws Exception {
        LRULinkedMap map = new LRULinkedMap(3) ;
        map.put("1",1);
        map.put("2",2);
        map.put("3",3);

        for (Map.Entry e : map.getAll()){
            System.out.print(e.getKey() + " : " + e.getValue() + "	");
        }

        System.out.println("");
        map.put("4",4);
        for (Map.Entry e : map.getAll()){
            System.out.print(e.getKey() + " : " + e.getValue() + "	");
        }
    }
    
//輸出
1 : 1    2 : 2    3 : 3    
2 : 2    3 : 3    4 : 4        

使用時:

    @Test
    public void get() throws Exception {
        LRULinkedMap map = new LRULinkedMap(4) ;
        map.put("1",1);
        map.put("2",2);
        map.put("3",3);
        map.put("4",4);

        for (Map.Entry e : map.getAll()){
            System.out.print(e.getKey() + " : " + e.getValue() + "	");
        }

        System.out.println("");
        map.get("1") ;
        for (Map.Entry e : map.getAll()){
            System.out.print(e.getKey() + " : " + e.getValue() + "	");
        }
    }

}

//輸出
1 : 1    2 : 2    3 : 3    4 : 4    
2 : 2    3 : 3    4 : 4    1 : 1

LinkedHashMap 內部也有維護一個雙向隊列,在初始化時也會給定一個緩存大小的閾值。初始化時自定義是否需要刪除最近不常使用的數據,如果是則會按照實現二中的方式管理數據。

其實主要代碼就是重寫了 LinkedHashMap 的 removeEldestEntry 方法:

    protected boolean removeEldestEntry(Map.Entry eldest) {
        return false;
    }

它默認是返回 false,也就是不會管有沒有超過閾值。

所以我們自定義大于了閾值時返回 true,這樣 LinkedHashMap 就會幫我們刪除最近最少使用的數據。

總結

以上就是對 LRU 緩存的實現,了解了這些至少在平時使用時可以知其所以然。

當然業界使用較多的還有 guava 的實現,并且它還支持多種過期策略。

號外

最近在總結一些 Java 相關的知識點,感興趣的朋友可以一起維護。

地址: https://github.com/crossoverJie/Java-Interview

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68959.html

相關文章

  • Guava 源碼分析(Cache 原理)

    摘要:緩存本次主要討論緩存。清除數據時的回調通知。具體不在本次的討論范圍。應該是以下原因新起線程需要資源消耗。維護過期數據還要獲取額外的鎖,增加了消耗。 showImg(https://segmentfault.com/img/remote/1460000015272232); 前言 Google 出的 Guava 是 Java 核心增強的庫,應用非常廣泛。 我平時用的也挺頻繁,這次就借助日...

    wangxinarhat 評論0 收藏0
  • Guava 源碼分析(Cache 原理)

    摘要:緩存本次主要討論緩存。清除數據時的回調通知。具體不在本次的討論范圍。應該是以下原因新起線程需要資源消耗。維護過期數據還要獲取額外的鎖,增加了消耗。 showImg(https://segmentfault.com/img/remote/1460000015272232); 前言 Google 出的 Guava 是 Java 核心增強的庫,應用非常廣泛。 我平時用的也挺頻繁,這次就借助日...

    Thanatos 評論0 收藏0
  • 一個線程安全的 lrucache 實現 --- 讀 leveldb 源碼

    摘要:在閱讀的源代碼的時候,發現其中的類正是一個線程安全的實現,代碼非常優雅。至此一個線程安全的類就已經全部實現,在中使用的緩存是,其實就是聚合多個實例,真正的邏輯都在類中。 緩存是計算機的每一個層次中都是一個非常重要的概念,緩存的存在可以大大提高軟件的運行速度。Least Recently Used(lru) cache 即最近最久未使用的緩存,多見與頁面置換算法,lru 緩存算法在緩存的...

    widuu 評論0 收藏0
  • NPM酷庫:lru-cache 基于內存的緩存管理

    摘要:酷庫,每天兩分鐘,了解一個流行庫。而直接將數據保存在程序變量中,最經濟快捷。但是這樣就會帶來一些其他問題,比如緩存更新緩存過期等。用于在內存中管理緩存數據,并且支持算法??梢宰尦绦虿灰蕾嚾魏瓮獠繑祿鞂崿F緩存管理。 NPM酷庫,每天兩分鐘,了解一個流行NPM庫。 為了優化程序性能,我們常常需要獎數據緩存起來,根據實際情況,我們可以將數據存儲到磁盤、數據庫、redis等。 但是有時候要緩...

    Yumenokanata 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<