1. 程式人生 > >Java實現簡易的快取

Java實現簡易的快取

之前看redis的書,書上羅列原始碼的時候我總是在想,redis為什麼不用Java實現!!

今天自己用Java寫了一個簡易的快取,發現,redis不用Java實現可能是正確的:C語言可以自行回收記憶體,而Java不可以(我水平可能沒達到,還沒有自己回收過某個物件的記憶體),這樣就導致了你的快取中的物件有可能都過期了,你只是把這些過期物件的引用置空,但是什麼時候回收這些記憶體,不是我們說了算的。

好,廢話不多說了,上一段程式碼(程式碼如果寫的不對、不合理的地方,還請大家一定指出)。

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MyCache {

  private Map<String, Value> map;
  private int threshold; //達到百分之多少回收
  private MemoryCalculateStrategy memoryCalculateStrategy;
  
  public MyCache() {//todo: 改成單例模式
    this(80, new SimpleMemoryCalculateStrategy());
  }

  public MyCache(int threshold) {
    this(threshold, new SimpleMemoryCalculateStrategy());
  }

  public MyCache(int threshold, MemoryCalculateStrategy memoryCalculateStrategy) {
    map = new ConcurrentHashMap<>();
    this.threshold = threshold;
    this.memoryCalculateStrategy = memoryCalculateStrategy;//todo: 改成工廠方法,會使用者友好一些
  }

  class Value {
    Object value;
    long expireTime;

    public Value(Object value, long expireTime) {
      this.value = value;
      this.expireTime = expireTime;
    }
  }

  public boolean put(String k, Object v, Long timeToLive) {
    checkMem();
    long expireTime = System.currentTimeMillis() + timeToLive * 1000;
    Value value = new Value(v, expireTime);
    map.put(k, value);
    return true;
  }

  private void checkMem() {
    if (!memoryCalculateStrategy.calculate(threshold)) return;
    synchronized (this){
      if (memoryCalculateStrategy.calculate(threshold)) {
        System.out.println("starting gc:記憶體佔用百分比:"+(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory())*100/Runtime.getRuntime().totalMemory());
        gc();
      }
    }
  }

  private void gc() {
    for (String key : map.keySet()) {
      long nowTime = System.currentTimeMillis();
      if (map.get(key).expireTime < nowTime) {
        map.remove(key);
      }
    }
    System.gc();
    System.out.println("end gc:記憶體佔用百分比:"+(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory())*100/Runtime.getRuntime().totalMemory());
  }

  public Object get(String k) {
    long nowTime = System.currentTimeMillis();
    Value value = map.get(k);
    if (value == null) return null;
    if (value.expireTime < nowTime) {
      map.remove(k);  //lazy
    }
    return (map.get(k)).value;
  }

  public static void main(String[] args) {//test code
    MyCache myCache = new MyCache();
    for(int i=0;i<10;i++){
      new Thread(() -> {
        for (int i1 = 0; i1 < 10000000; i1++) {
          List list = new ArrayList();
          for (int j = 0; j < 100; j++) {
            list.add(j);
          }
          myCache.put(i1 + "", list, 1L);
        }
      }).start();
    }
  }
}
public interface MemoryCalculateStrategy {
  boolean calculate(int threshold);
}
public class SimpleMemoryCalculateStrategy implements MemoryCalculateStrategy {
  @Override
  public boolean calculate(int threshold) {
    long usedMemorySize = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
    long maxMemorySize = Runtime.getRuntime().totalMemory(); //最大可用記憶體
    if (usedMemorySize * 100 / maxMemorySize > threshold) return true;
    return false;
  }
}

因快取系統總會存在多個執行緒操作一個物件的情況,故用了執行緒安全的J.U.C.ConcurrentHashMap,因此正常情況下的put和set操作會在常數時間內完成。

呼叫構造方法的時候,可以自己限定記憶體用到百分之多少的時候,來遍歷清除過期物件。

get時,如果值過期,則會將該物件從map中移除(但是並不是立即回收該物件的記憶體,下一次GC時才會回收)

設計時為了避免多個執行緒同時清理記憶體,因此此處要保證執行緒安全,但是這時一個快取系統,效率不能太低,因此採用了double-check(參考了Spring的做法,Rod Johnson你聽我解釋........這不是抄襲,真的),這樣可以既保證執行緒安全,效率也不會很低。

我程式碼中會有不合理的地方,比如,回收記憶體的時候直接System.gc(),這樣會導致full gc,會stop the world,如果大家好的想法,還請大家指出來。

好了,雖說很多情況下不適合用Java編寫,絲毫不撼動我愛Java的心~~

不知以後如果喜歡上別的語言,再看到這裡會是什麼心情。。。。。