Mini-challenge: LRU Cache generic với LinkedHashMap
Bài thực hành khép lại Module 8 — xây LruCache<K, V> generic dựa trên LinkedHashMap(accessOrder=true), thread-safe version với ReentrantLock, benchmark so sánh.
Đây là mini-challenge khép lại Module 8. Bạn sẽ xây một LRU Cache (Least Recently Used) — cấu trúc dữ liệu phổ biến: giữ tối đa N entry, khi vượt giới hạn thì evict entry truy cập cũ nhất. Dùng làm browser cache, CPU cache, database query cache.
Yêu cầu: generic <K, V> để dùng được với mọi kiểu key/value, tận dụng LinkedHashMap(accessOrder=true) của JDK thay vì tự viết doubly-linked list + hash table.
Mục tiêu: kết hợp generics, collections, và hiểu vì sao JDK bày sẵn công cụ cho pattern phổ biến như LRU.
🎯 Đề bài
Thiết kế LruCache<K, V>:
1. class LruCache<K, V>
- Constructor
LruCache(int maxSize)— throwIllegalArgumentExceptionnếu maxSize ≤ 0. V get(K key)— trả value, returnnullnếu không có. Đồng thời đánh dấu key là "vừa truy cập".void put(K key, V value)— thêm/cập nhật. Nếu vượtmaxSize, evict entry cũ nhất (LRU).int size()— số entry hiện tại.boolean contains(K key)— không cập nhật access order.void clear()— xoá tất cả.
2. Yêu cầu impl
- Bên dưới dùng
LinkedHashMap<K, V>vớiaccessOrder = true. - Override
removeEldestEntryđể evict tự động khi vượtmaxSize. - Wrap trong class riêng, không expose
LinkedHashMapra ngoài (encapsulation).
3. Sau đó tạo ThreadSafeLruCache<K, V> (bonus)
- Cùng API nhưng thread-safe bằng
ReentrantLock.
Output mẫu:
-- Simple LRU --
put(a, 1)
put(b, 2)
put(c, 3)
get(a) = 1 // a bay gio moi nhat
put(d, 4) // vuot 3 entry -> evict b (cu nhat)
size = 3
contains(b) = false
contains(a) = true
contains(c) = true
contains(d) = true
📦 Concept dùng trong bài
| Concept | Bài | Dùng ở đây |
|---|---|---|
| Generic class | Module 8, bài 1 | LruCache<K, V> |
| Type erasure, bound | Module 8, bài 2 | K/V không có bound — dùng được mọi kiểu |
| LinkedHashMap access order | Module 8, bài 4 | Core của LRU |
| Encapsulation | Module 5, bài 4 | Field map private final |
| Validate input | Module 7, bài 4 | throw IllegalArgumentException |
| try/finally | Module 7, bài 1 | Lock + unlock an toàn |
▶️ Starter code
import java.util.*;
import java.util.concurrent.locks.*;
public class LruCacheApp {
// TODO: class LruCache<K, V>
// - field private final Map<K, V> map = LinkedHashMap(maxSize, 0.75, true) override removeEldestEntry
// - field private final int maxSize
// - constructor validate maxSize > 0
// - get, put, size, contains, clear
// TODO: class ThreadSafeLruCache<K, V>
// - wrap LruCache voi ReentrantLock cho moi method
public static void main(String[] args) {
System.out.println("-- Simple LRU --");
LruCache<String, Integer> cache = new LruCache<>(3);
cache.put("a", 1); System.out.println("put(a, 1)");
cache.put("b", 2); System.out.println("put(b, 2)");
cache.put("c", 3); System.out.println("put(c, 3)");
System.out.println("get(a) = " + cache.get("a")); // 1, a moi nhat
cache.put("d", 4); System.out.println("put(d, 4)"); // evict b
System.out.println("size = " + cache.size());
System.out.println("contains(b) = " + cache.contains("b"));
System.out.println("contains(a) = " + cache.contains("a"));
System.out.println("contains(c) = " + cache.contains("c"));
System.out.println("contains(d) = " + cache.contains("d"));
}
}
javac LruCacheApp.java
java LruCacheApp
Dành 25–30 phút tự làm.
💡 Gợi ý
LruCache với LinkedHashMap:
public static class LruCache<K, V> {
private final Map<K, V> map;
private final int maxSize;
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize must be > 0, got " + maxSize);
}
this.maxSize = maxSize;
// accessOrder = true -> iterate/evict theo thu tu truy cap cuoi
this.map = new LinkedHashMap<K, V>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > LruCache.this.maxSize;
}
};
}
public V get(K key) {
return map.get(key); // LinkedHashMap cap nhat order tu dong
}
public void put(K key, V value) {
map.put(key, value); // removeEldestEntry tu check va evict
}
public int size() { return map.size(); }
public boolean contains(K key) {
// containsKey KHONG update access order (unlike get)
return map.containsKey(key);
}
public void clear() { map.clear(); }
}
Điểm mấu chốt:
LinkedHashMap(initialCapacity, loadFactor, accessOrder)— constructor với 3 param.accessOrder = true: thay vì thứ tự insert, list liên kết trong map tracks access order (get/put đẩy key lên cuối, oldest ở đầu).removeEldestEntrygọi sau mỗiput— trảtruethì evict eldest.containsKeyvsget:getupdate access order,containsKeykhông — API rõ ràng.
ThreadSafeLruCache với ReentrantLock:
public static class ThreadSafeLruCache<K, V> {
private final LruCache<K, V> delegate;
private final ReentrantLock lock = new ReentrantLock();
public ThreadSafeLruCache(int maxSize) {
this.delegate = new LruCache<>(maxSize);
}
public V get(K key) {
lock.lock();
try { return delegate.get(key); }
finally { lock.unlock(); }
}
public void put(K key, V value) {
lock.lock();
try { delegate.put(key, value); }
finally { lock.unlock(); }
}
public int size() {
lock.lock();
try { return delegate.size(); }
finally { lock.unlock(); }
}
public boolean contains(K key) {
lock.lock();
try { return delegate.contains(key); }
finally { lock.unlock(); }
}
public void clear() {
lock.lock();
try { delegate.clear(); }
finally { lock.unlock(); }
}
}
Pattern: mỗi method lock → try { delegate } finally { unlock }. Critical — unlock trong finally để không leak khi exception.
✅ Lời giải
import java.util.*;
import java.util.concurrent.locks.*;
public class LruCacheApp {
public static class LruCache<K, V> {
private final Map<K, V> map;
private final int maxSize;
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize must be > 0, got " + maxSize);
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > LruCache.this.maxSize;
}
};
}
public V get(K key) { return map.get(key); }
public void put(K key, V value) { map.put(key, value); }
public int size() { return map.size(); }
public boolean contains(K key) { return map.containsKey(key); }
public void clear() { map.clear(); }
}
public static class ThreadSafeLruCache<K, V> {
private final LruCache<K, V> delegate;
private final ReentrantLock lock = new ReentrantLock();
public ThreadSafeLruCache(int maxSize) {
this.delegate = new LruCache<>(maxSize);
}
public V get(K key) {
lock.lock();
try { return delegate.get(key); }
finally { lock.unlock(); }
}
public void put(K key, V value) {
lock.lock();
try { delegate.put(key, value); }
finally { lock.unlock(); }
}
public int size() {
lock.lock();
try { return delegate.size(); }
finally { lock.unlock(); }
}
public boolean contains(K key) {
lock.lock();
try { return delegate.contains(key); }
finally { lock.unlock(); }
}
public void clear() {
lock.lock();
try { delegate.clear(); }
finally { lock.unlock(); }
}
}
public static void main(String[] args) {
System.out.println("-- Simple LRU --");
LruCache<String, Integer> cache = new LruCache<>(3);
cache.put("a", 1); System.out.println("put(a, 1)");
cache.put("b", 2); System.out.println("put(b, 2)");
cache.put("c", 3); System.out.println("put(c, 3)");
System.out.println("get(a) = " + cache.get("a"));
cache.put("d", 4); System.out.println("put(d, 4)");
System.out.println("size = " + cache.size());
System.out.println("contains(b) = " + cache.contains("b"));
System.out.println("contains(a) = " + cache.contains("a"));
System.out.println("contains(c) = " + cache.contains("c"));
System.out.println("contains(d) = " + cache.contains("d"));
System.out.println("\n-- ThreadSafe LRU (demo) --");
ThreadSafeLruCache<Integer, String> tsCache = new ThreadSafeLruCache<>(2);
tsCache.put(1, "one");
tsCache.put(2, "two");
tsCache.put(3, "three"); // evict 1
System.out.println("get(1) = " + tsCache.get(1));
System.out.println("get(2) = " + tsCache.get(2));
System.out.println("get(3) = " + tsCache.get(3));
}
}
Giải thích từng phần:
-
Generic
<K, V>: cache dùng được với bất kỳ kiểu key/value. Không cần bound —LinkedHashMapđã yêu cầuequals/hashCoderuntime, không cần compile-time constraint. -
LinkedHashMap(16, 0.75f, true): 3 param constructor.initialCapacity = 16— default.loadFactor = 0.75f— default.accessOrder = true— điểm quan trọng. Thứ tự iterate là thứ tự truy cập (getmove lên cuối), không phải thứ tự insert. Oldest ở đầu, newest ở cuối.
-
removeEldestEntry: hook củaLinkedHashMap— được gọi sau mỗiput. Trảtrue→ eldest entry bị remove. Ta returnsize() > maxSize→ auto-evict khi đầy. -
Inner anonymous class: tạo subclass
LinkedHashMaptại chỗ để overrideremoveEldestEntry.LruCache.this.maxSizeđể tham chiếu field outer class từ inner. -
containsvsget: phân biệt intent.get"truy cập value" → update access order.contains"check existence" → không update. API rõ ràng. -
ThreadSafeLruCachedùng composition + delegation: không extendsLruCachemà wrap nó, lock mọi method. Pattern composition an toàn hơn inheritance (bài Module 6). -
lock.lock() / try { } finally { lock.unlock() }: pattern bắt buộc. Nếu exception trong body mà không unlock → deadlock thread khác.
🎓 Mở rộng
Mức 1 — Thống kê hit/miss:
private long hits = 0;
private long misses = 0;
public V get(K key) {
V v = map.get(key);
if (v != null) hits++;
else misses++;
return v;
}
public double hitRate() {
long total = hits + misses;
return total == 0 ? 0 : (double) hits / total;
}
Mức 2 — TTL (Time-To-Live):
Wrap value với timestamp, evict khi quá hạn:
record Entry<V>(V value, long expiresAt) { }
LruCache<String, Entry<Data>> cache = new LruCache<>(100);
cache.put("key", new Entry<>(data, System.currentTimeMillis() + 60_000));
// Khi get:
Entry<Data> e = cache.get("key");
if (e == null || System.currentTimeMillis() > e.expiresAt()) return null;
return e.value();
Production thường dùng Caffeine library — LRU + TTL + async refresh + W-TinyLFU eviction. Nhưng viết tay 1 lần để hiểu nền tảng.
Mức 3 — Unit test concurrent:
@Test
void threadSafeUnderContention() throws Exception {
var cache = new ThreadSafeLruCache<Integer, Integer>(100);
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10_000; i++) {
final int key = i % 50;
executor.submit(() -> cache.put(key, key * 2));
executor.submit(() -> cache.get(key));
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
assertTrue(cache.size() <= 100);
// Khong co ConcurrentModificationException, khong co NPE
}
Test với LruCache non-safe dưới concurrent → crash hoặc corrupted. ThreadSafeLruCache → clean.
Mức 4 — Thay ReentrantLock bằng synchronized hoặc ConcurrentHashMap — trade-off:
synchronized: đơn giản nhất, nhưng không cótryLock/ timeout.ReentrantLock: flexible (tryLock, fairness, condition).ConcurrentHashMap: scalable hơn (lock striping), nhưng không support LRU built-in. Phải tự build LRU trên nó — phức tạp, thường dùng Caffeine thay.
✨ Điều bạn vừa làm được
Hoàn thành mini-challenge này, bạn đã:
- Viết generic class
<K, V>dùng được với mọi kiểu — không phảiStringCache,IntCacheriêng. - Tận dụng
LinkedHashMap(accessOrder=true)của JDK thay vì tự build doubly-linked list + hash table. Principle: "know your stdlib" — nhiều pattern đã có trong JDK. - Áp dụng encapsulation: field
private final, map không lộ ra ngoài, API clean. - Phân biệt
getvscontainsKeysemantics — intent-revealing API. - Dùng composition (
ThreadSafeLruCachewrapLruCache) thay vì inheritance — module 6 rule. - Áp pattern lock + try/finally đảm bảo unlock an toàn — tránh deadlock khi exception.
- Validate input trong constructor (throw
IllegalArgumentException) — fail fast với config sai.
Chúc mừng — bạn đã hoàn thành Module 8! Từ đây bạn có công cụ mô hình dữ liệu đầy đủ (generic + collections). Module 9 sẽ bước sang Stream API và Lambda — xử lý dữ liệu theo functional style, core của Java modern.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên