如何用JDK優雅的實現幾個演算法
今天給大家介紹下八股文中的兩個重要成員,LinkedHashMap和TreeMap。
這兩者都實現了Map介面,也經常會在面試中被拿來與HashMap比較。
到底它們能使用哪些魔法呢,接下來,就讓我們開啟探祕之旅。
前言
首先我們來看一下HashMap,這玩意兒可是個八股的高頻考點,你要是講不出個1 2 3 4 5來,面試官可是會不高興的。
當初有寫過一篇《HashMap在多執行緒下資料丟失問題》文章,大概講了一下HashMap的執行緒安全問題。其中也涉及到了一些初始化,負載因子,解決雜湊衝突的方法等。並且解釋了個魔法版的在多執行緒情況下也能資料不丟失的情況(說是不丟,但其實也有問題。具體感興趣的朋友可以直接百度搜索下文章看看,並沒有放到公眾號中)。
此處我就大概的描述下。
HashMap的知識點,大致有以下這麼多點:
- HashMap是執行緒不安全的
- HashMap無序
- 底層資料結構在1.8之後為陣列+連結串列+紅黑樹
- 連結串列和紅黑樹的轉換
- 1.8之後連結串列由頭插法變為尾插
- 擴容機制
- 負載因子以及初始化時的特別操作
- 容量的取值的特別操作
- Hash演算法的優雅之處
- 計算雜湊槽的優雅之處
- 其他解決雜湊衝突的方法
- .............
基本上問的內容都不會脫離上面列的知識點了吧,當然也可能是我眼界問題,如果有缺漏的也希望廣大朋友們指出。
由於並不是本篇的重點,所以只列出可能有的知識點,感興趣的小夥伴可以再細緻的瞭解下。
LinkedHashMap
這東西繼承於HashMap,所以LinkedHashMap大部分的特性和HashMap是一致的。
至於為什麼說是大部分特性呢?這是因為LinkedHashMap本身其實是HashMap + 連結串列的資料結構,所以它是有序的。
而且它有兩種順序選擇。1:按元素插入順序,2:按元素訪問順序。是通過內部的域(accessOrder)來控制的,可以在構造時指定,為true表示按元素訪問順序。
朋友們,如果單純說按元素訪問順序排序你會想到什麼?這不妥妥的實現LRU演算法的邏輯嗎?再想想我們之前的LRU演算法是怎麼實現的,內部資料介面就是連結串列+陣列呀。
所以是的,用LinkedHashMap也是可以實現LRU演算法的。不過也並不能直接用,需要做些改造,接下來我們就一起來看看吧。
public class LRUCache<K,V> extends LinkedHashMap<K,V> { private int maxCapacity; @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > maxCapacity; } public void setMaxCapacity(int maxCapacity) { this.maxCapacity = maxCapacity; } }
怎麼樣,是不是看著很簡單。內部的邏輯都由JDK幫我們實現好了。
咱們主要實現removeEldestEntry
方法,這個方法是在LinkedHashMap有呼叫到,在每次put時呼叫removeEldestEntry
方法判斷是否需要移除最舊未訪問到的元素。詳細程式碼如下:
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } }
可以看到相比於我們自己基於連結串列+陣列的實現方式,簡潔很多,而且通常情況下也更不容易出錯。因此活用JDK自己的實現是一種很好的工作方式,也能更有效率。當然,該瞭解的原理咱們也還是得了解的。
TreeMap
這玩意兒吧,業務中好像是挺少用到的。但實際上,它使用場景非常廣泛,比如大名鼎鼎的一致性雜湊演算法,就可以使用TreeMap來實現。具體如何,請聽我細細道來。
TreeMap底層資料結構是紅黑樹,是一種自平衡二叉排序樹(但是又非嚴格的平衡),更傾向於區域性的平衡,通常通過左旋或者右旋來維持自身的平衡。具體我就不詳說了(主要是還挺複雜的,真的要詳細的話可以單獨水開一章)。
一致性雜湊演算法又是啥呢,這東西,八股也經常考。這是在分散式領域裡非常重要的一個演算法,用於解決多節點情況下的負載演算法。
主要思路是將多個節點組成一個圓,當有流量訪問時計算對應的hash值,然後找出大於該hash值的最近的一個節點,將該流量分配。
Redis叢集和RocketMq內部就有使用這種演算法來做負載均衡。如此當有一個節點失效或者插入一個新節點時,也只會有部分資料需要遷移或者直接失效而已。
測試方法如下:
public class TreeMapMain { public static void main(String[] args) { TreeMap<Integer, String> nodeMap = new TreeMap<>(); nodeMap.put(RandomUtils.nextInt(), "node1"); nodeMap.put(RandomUtils.nextInt(), "node2"); nodeMap.put(RandomUtils.nextInt(), "node3"); System.out.println(nodeMap); for (int i = 0; i < 10; i++) { int hashCode = RandomUtils.nextInt(); System.out.println(hashCode); SortedMap<Integer, String> tailMap = nodeMap.tailMap(hashCode); Integer matchNodeKey = !tailMap.isEmpty() ? tailMap.firstKey() : nodeMap.firstKey(); System.out.println(nodeMap.get(matchNodeKey)); } } }
主要就是SortedMap<Integer, String> tailMap = nodeMap.tailMap(hashCode);
這一段,tailMap方法語義是返回大於等於key的子樹。再通過firstKey找到最小的一個節點,如此就完成了一致性雜湊演算法。所以理念上雖然會感覺複雜,但是基於JDK我們還是可以很簡單的實現。
有朋友或許就會問了,如果雜湊分佈不合理,3個節點並不是均等分佈的怎麼辦呢?就比如一共100個數,node1是0-50,node2是51-90,node3是91-100。如此的分發,流量完全不對等,會出現部分節點負載過高,部分節點又很閒置。
這個問題問得很好,不過也別擔心,業界也已經有很合理的解法了,那就是虛擬節點。將多個虛擬節點對映成1個真實的節點,當訪問虛擬節點時其實就是訪問真實節點了。如此節點分得越細,就越不容易出現負載不均的問題了。
圖例和程式碼就不詳細展示了,免得像水字數,就當我是提供了個思路給廣大的小夥伴,讓小夥伴自己思考一下圖解與詳細程式碼的實現吧。
最後
今天的分享到這裡就結束了,寫的沒有特別詳細,整體更像是提供了一個思路,也留了部分的坑留待後續填上。
今天我們主要是列舉了一些HashMap的知識點,以及LinkedHashMap的內部儲存結構和可以用到的場景,另外就是講解了一下一致性雜湊演算法以及通過TreeMap如何簡單的實現它。
希望大家能有收穫,我們下期再見~