Đâ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 ý
💡 💡 Gợi ý — đọc khi bị kẹt
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
ℹ️ ✅ Lời giải — xem sau khi đã thử
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.