上一章分析了mybatis的原始碼的日誌模組,像我們經常說的mybatis一級快取,二級快取,快取究竟在底層是怎樣實現的。此次開始分析快取模組
1. 原始碼位置,mybatis原始碼包位於org.apache.ibatis.cache下,如圖
2. 先從org.apache.ibatis.cache下的cache介面開始
// 快取介面 public interface Cache { // 獲取快取ID String getId(); // 放入快取 void putObject(Object key, Object value); // 獲取快取 Object getObject(Object key); // 移除某一快取 Object removeObject(Object key); // 清除快取 void clear(); // 獲取快取大小 int getSize(); // 獲取鎖 ReadWriteLock getReadWriteLock(); }
mybatis提供了自定義的快取介面,功能通俗易懂,沒什麼好解釋的。有介面,必然有實現,看一下快取介面的基本實現類PerpetualCache,所在路徑為org.apache.ibatis.cache.impl下。
public class PerpetualCache implements Cache { // 快取的ID private String id; // 使用HashMap充當快取(老套路,快取底層實現基本都是map) private Map<Object, Object> cache = new HashMap<Object, Object>(); // 唯一構造方法(即快取必須有ID) public PerpetualCache(String id) { this.id = id; } // 獲取快取的唯一ID public String getId() { return id; } // 獲取快取的大小,實際就是hashmap的大小 public int getSize() { return cache.size(); } // 放入快取,實際就是放入hashmap public void putObject(Object key, Object value) { cache.put(key, value); } // 從快取獲取,實際就是從hashmap中獲取 public Object getObject(Object key) { return cache.get(key); } // 從快取移除 public Object removeObject(Object key) { return cache.remove(key); } // hashmap清除資料方法 public void clear() { cache.clear(); } // 暫時沒有其實現 public ReadWriteLock getReadWriteLock() { return null; } // 快取是否相同 public boolean equals(Object o) { if (getId() == null) throw new CacheException("Cache instances require an ID."); if (this == o) return true; // 快取本身,肯定相同 if (!(o instanceof Cache)) return false; // 沒有實現cache類,直接返回false Cache otherCache = (Cache) o; // 強制轉換為cache return getId().equals(otherCache.getId()); // 直接比較ID是否相等 } // 獲取hashCode public int hashCode() { if (getId() == null) throw new CacheException("Cache instances require an ID."); return getId().hashCode(); } }
如上分析,mybatis的基本快取實現類其實就是內部維護了一個HashMap,通過對HashMap操作來實現基本的功能。但需要注意的是,判斷兩個快取是否相等,是比較的快取ID是否相等。看Cache otherCache = (Cache) o;也就是說快取介面可能有多種實現,也確實如此。PerpetualCache只提供了快取的基本實現功能,但一看HashMap就是不安全的類,多執行緒下肯定會出問題。又比如說我想這個快取有固定大小,快取過期策越為先進先出或者LRU功能等。myabtis肯定想到這點,檢視org.apache.ibatis.cache.decorators包。看名字就知道用到了裝飾者模式。檢視包下的類,如SynchronizedCache為快取保障了執行緒安全,LruCache定義了快取的過期策略為淘汰最近最少訪問的資料,LoggIngCache提供了日誌列印功能。使用者想讓自己的快取具備什麼功能,就使用這些裝飾者類進行裝飾。
3. 分析快取裝飾類SynchronizedCache
// 在操作前加鎖,保證執行緒安全 @Override public synchronized int getSize() { return delegate.getSize(); } @Override public synchronized void putObject(Object key, Object object) { delegate.putObject(key, object); } @Override public synchronized Object getObject(Object key) { return delegate.getObject(key); } @Override public synchronized Object removeObject(Object key) { return delegate.removeObject(key); } @Override public synchronized void clear() { delegate.clear(); }
很簡單。就是在方法前使用synchronized加鎖,保證執行緒安全。
4. 分析快取裝飾類LruCache
介紹LruCache前,先介紹下Lru的實現,Lru是很常用的淘汰策略,意為最近最少使用的物件。檢視LruCache,發現內部使用了LinkedHashMap,熟悉LinkedHashMap的夥伴應該知道了。我們一般手寫LRU功能就是通過複寫LinkedHashMap的方法來實現,LruCache也一樣。先大致瞭解下LinkedHashMap。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
LinkedHashMap繼承HashMap類,實際上就是對HashMap的一個封裝。
// 內部維護了一個自定義的Entry,整合HashMap中的node類 static class Entry<K,V> extends HashMap.Node<K,V> { // linkedHashmap用來連線節點的欄位,根據這兩個欄位可查詢按順序插入的節點 Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
檢視LinkedHashMap構造方法,具體訪問順序見下文分析
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { // 呼叫HashMap的構造方法 super(initialCapacity, loadFactor); // 訪問順序維護,預設false不開啟 this.accessOrder = accessOrder; }
引入兩張圖來理解下HashMap和LinkedHashMap
以上時HashMap的結構,採用拉鍊法解決衝突。LinkedHashMap在HashMap基礎上增加了一個雙向連結串列來表示節點插入順序。
如上,節點上多出的紅色和藍色箭頭代表了Entry中的before和after。在put元素時,會自動在尾節點後加上該元素,維持雙向連結串列。瞭解LinkedHashMap結構後,在看看究竟什麼是維護節點的訪問順序。先說結論,當開啟accessOrder後,在對元素進行get操作時,會將該元素放在雙向連結串列的隊尾節點。原始碼如下:
public V get(Object key) { Node<K,V> e; // 呼叫HashMap的getNode方法,獲取元素 if ((e = getNode(hash(key), key)) == null) return null; // 預設為false,如果開啟維護連結串列訪問順序,執行如下方法 if (accessOrder) afterNodeAccess(e); return e.value; } // 方法實現(將e放入尾節點處) void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; // 當節點不是雙向連結串列的尾節點時 if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 將待調整的e節點賦值給p p.after = null; if (b == null) // 說明e為頭節點,將老e的下一節點值為頭節點 head = a; else b.after = a;// 否則,e的上一節點直接指向e的下一節點 if (a != null) a.before = b; // e的下一節點的上節點為e的上一節點 else last = b; if (last == null) head = p; else { p.before = last; // last和p互相連線 last.after = p; } tail = p; // 將雙向連結串列的尾節點指向p ++modCount; // 修改次數加以 } }
程式碼很簡單,如上面的圖,我訪問了節點值為3的節點,那木經過get操作後,結構變成如下
經過如上分析我們知道,如果限制雙向連結串列的長度,每次刪除頭節點的值,就變為一個lru的淘汰策略了。舉個例子,我想限制雙向連結串列的長度為3,依次put 1 2 3,連結串列為 1 -> 2 -> 3,訪問元素2,連結串列變為 1 -> 3-> 2,然後put 4 ,發現連結串列長度超過3了,淘汰1,連結串列變為3 -> 2 ->4;
那木linkedHashMap是怎樣知道自定義的限制策略,看程式碼,因為LinkedHashMap中沒有提供自己的put方法,是直接呼叫的HashMap的put方法,檢視hashMap程式碼如下:
// hashMap final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); // 看這個方法 afterNodeInsertion(evict); return null; } // linkedHashMap重寫了此方法 void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; // removeEldestEntry預設返回fasle if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; // 移除雙向連結串列中的頭指標元素 removeNode(hash(key), key, null, false, true); } }
大功告成。原來只需要重新實現removeEldestEntry就可以自定義實現lru功能了。
下文分析LruCache就好多了。