LRU Cache (Least Recently Used Cache) — gyorsítótárazási algoritmus, amely kiszorítja a legrégebben nem használt elemeket, amikor a gyorsítótár mérete eléri a határt. Minden olvasáskor vagy íráskor az elem a sor elejére kerül, túlcsorduláskor pedig a végén lévő elem törlődik. A Android Developers (2026) dokumentációja szerint az Android LruCache osztálya LinkedHashMap-et használ access-order módban, és O(1) komplexitást biztosít a get és put műveletekhez.
Főbb pontok
LRU Cache (Least Recently Used Cache) — egy fix méretű adatstruktúra, amely korlátozott számú elemet tárol, és automatikusan eltávolítja azokat, amelyekhez a legritkábban fértek hozzá. Amikor egy alkalmazás lekér egy elemet, az a gyorsítótár „friss” részébe kerül, a régen nem használt elemek pedig a vége felé tolódnak, és a határ elérésekor törlődnek.
A „Least Recently Used” elnevezés a kiszorítási politikát írja le: a tárolt elemek közül a legrégebben nem használt kerül eltávolításra. Ez a lokalitási elven (locality of reference) alapul — a közelmúltban lekért adatok nagy valószínűséggel újra szükségesek lesznek. Ezért tekintik az LRU-t a legtöbb alkalmazás számára az egyik leghatékonyabb gyorsítótárazási stratégiának.
A klasszikus LRU Cache implementáció két adatstruktúrát igényel: egy hash táblát az O(1) hozzáféréshez bármely elemhez kulcs alapján, és egy duplán láncolt listát a használati sorrend nyomon követéséhez. A hash tábla referenciákat tárol a lista csomópontjaira, a lista pedig a legújabb elemtől (fej) a legrégebbiig (farok) tartja a sorrendet.
A get(key) művelet ellenőrzi a kulcs jelenlétét a hash táblában. Ha az elem megtalálható, a lista elejére kerül (a legújabb lesz), és az értéke visszaadásra kerül. Ha nem található — null visszaadása vagy kivétel dobása történik. A put(key, value) művelet új elemet szúr be: ha a kulcs már létezik — az érték frissül és az elem a fejre kerül. Ha a gyorsítótár tele van, a beszúrás előtt a lista farok eleme törlődik. Minden művelet konstans O(1) időben hajtódik végre.
Az LRU Cache algoritmus két elven alapul: egy időrendi hozzáférés-számláló és a túlcsorduláskori kiszorítás mechanikája. Minden elem a duplán láncolt lista egy csomópontjában tárolódik, a mutatók ezekre a csomópontokra — a hash táblában. Minden egyes elemhez való hozzáféréskor az leválasztásra kerül az aktuális pozícióról, és a lista elejére kerül.
Amikor a gyorsítótár mérete eléri a maximális értéket (maxSize), és új elem beszúrására irányuló kérés érkezik, az algoritmus eltávolítja a duplán láncolt lista farok elemét — ez a legrégebben nem használt elem. Az eltávolítás után hely szabadul fel az új elem számára, amely a lista fejére kerül. A hash tábla frissül: a régi kulcs törlődik, az új hozzáadódik.
Az LRU jellemzője — érzéketlenség a ciklikusan ismétlődő hozzáférési mintákra. Ha egy alkalmazás időszakosan a gyorsítótár méreténél nagyobb adathalmazhoz fér hozzá, az LRU thrashing-től szenvedhet — az elemek gyakori cseréjétől, amikor minden új kérés kiszorítja az előzőt. Ilyen forgatókönyvekben az LFU (Least Frequently Used) vagy az adaptív algoritmusok hatékonyabbak lehetnek.
Az LRU Cache méretének megválasztása kompromisszum a memóriafogyasztás és a hit-ratio (sikeres hozzáférések százaléka) között. Tipikus értékek mobilalkalmazásokhoz: a rendelkezésre álló memória 10–20%-a képgyorsítótárhoz és 50–200 bejegyzés hálózati válaszgyorsítótárhoz. A hit-ratio 80–95% jó mutatónak számít, amelynél a gyorsítótár indokolja a memóriaköltséget. A monitorozáshoz a hitCount és missCount számlálók használhatók, amelyek elérhetők az Android LruCache implementációjában.
A kanonikus LRU Cache implementáció egy hash tábla és egy duplán láncolt lista kombinációját használja. A hash tábla O(1) hozzáférést biztosít bármely csomóponthoz kulcs alapján, a duplán láncolt lista pedig — a csomópont fejre helyezését és a farokról való törlését O(1) időben. Fontos, hogy a lista pontosan duplán láncolt legyen: ez lehetővé teszi egy csomópont leválasztását a lista közepéről az összes elem bejárása nélkül.
class LruCache<K, V>(
private val maxSize: Int
) {
private val map = mutableMapOf<K, Node<V>>()
private val head = Node<V>(null)
private val tail = Node<V>(null)
init {
head.next = tail
tail.prev = head
}
fun get(key: K): V? {
val node = map[key] ?: return null
removeNode(node)
addToHead(node)
return node.value
}
fun put(key: K, value: V) {
map[key]?.let { node ->
removeNode(node)
node.value = value
addToHead(node)
return
}
if (map.size >= maxSize) {
tail.prev?.let { toRemove ->
removeNode(toRemove)
removeKeyByValue(toRemove)
}
}
val newNode = Node(value)
addToHead(newNode)
}
}
Az implementációban minden csomópont (Node) tárolja az értéket és a referenciákat az előző és következő csomópontra. Az őrszem csomópontok (head és tail) leegyszerűsítik a határeseteket — nincs szükség null ellenőrzésre beszúráskor és törléskor. A get metódus a megtalált csomópontot a fejre helyezi, a put pedig túlcsorduláskor törli a farok elemet. Egy külön removeKeyByValue metódus megkeresi a kulcsot a hash táblában a csomópont referenciája alapján, és eltávolítja azt.
Az Android SDK kész LruCache osztályt biztosít az android.util csomagban, amely az LRU algoritmust implementálja LinkedHashMap használatával access-order módban. Az osztály thread-safe, támogatja a hit/miss számlálást, valamint entryRemoved callback-et biztosít az erőforrások felszabadításához elem kiszorításakor. A gyorsítótár mérete tetszőleges egységekben állítható be (bájt, elemek száma) — elegendő felülírni a sizeOf metódust.
Mindhárom algoritmus — LRU, FIFO és LIFO — ugyanazt a feladatot oldja meg: a memóriafogyasztás korlátozását elemek kiszorításával túlcsorduláskor. Azonban alapvetően különböző kritériumokat használnak az áldozat kiválasztásához, ami meghatározza hatékonyságukat különböző forgatókönyvekben.
| Paraméter | LRU | FIFO | LIFO |
|---|---|---|---|
| Kiszorítási kritérium | Legrégebben nem használt | Elsőként hozzáadott | Utoljára hozzáadott |
| Adatstruktúra | HashMap + Doubly Linked List | Sor (Queue) | Verem (Stack) |
| get/put komplexitás | O(1) | O(1) | O(1) |
| Ellenállás mintáknak | Magas | Közepes | Alacsony |
| Tipikus alkalmazás | Kép- és adatgyorsítótár | Adatfolyamok pufferelése | Műveletek visszavonása (undo) |
FIFO a legrégebbi elemet szorítja ki a hozzáadás ideje alapján, függetlenül attól, hogy milyen gyakran fértek hozzá. Ez hatástalan lehet, ha a régi elem még mindig releváns. Az LRU elkerüli ezt a hátrányt a hozzáférési minta figyelembevételével. LIFO a frissen hozzáadott elemet szorítja ki — hasznos az undo forgatókönyvekhez, de nem alkalmas gyorsítótárazásra, mivel az új adatok gyakran szükségesebbek a régebbieknél. Az LRU optimális egyensúlynak tekinthető az implementáció komplexitása és a hit-ratio között a legtöbb alkalmazás számára.
Vizsgáljuk meg a beépített LruCache osztály használatát az Android SDK-ból betöltött képek gyorsítótárazásához. A példa a gyorsítótár inicializálását mutatja az alkalmazás rendelkezésre álló memóriájának 1/8-án, ami a Google szabványos ajánlása a képek gyorsítótárazásához.
import android.util.LruCache
class ImageCache(context: Context) {
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
private val cacheSize = maxMemory / 8
private val lruCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.rowBytes * bitmap.height / 1024
}
}
fun getBitmap(key: String): Bitmap? {
return lruCache.get(key)
}
fun putBitmap(key: String, bitmap: Bitmap) {
lruCache.put(key, bitmap)
}
}
A sizeOf metódus az elem méretét adja vissza ugyanabban a mértékegységben, mint a cacheSize. Itt a Bitmap mérete kilobájtban van használva (rowBytes × height / 1024). Amikor az összes elem sizeOf-jának összege meghaladja a cacheSize-t, az LruCache automatikusan kiszorítja a legrégebben nem használt Bitmap-eket. Az entryRemoved callback használható a bitmap.recycle() meghívásához — memória felszabadítása a kiszorítás előtt.
Az iOS nem rendelkezik beépített LRU Cache osztállyal, de könnyen implementálható NSCache segítségével (amely hasonló, de nem dokumentált kiszorítási politikát használ) vagy saját implementációval Dictionary + Doubly Linked List alapokon, ahogy az alábbiakban látható.
class LRUCache<Key: Hashable, Value> {
private let maxSize: Int
private var dict = [Key: Node<Value>]()
private var head: Node<Value>?
private var tail: Node<Value>?
init(maxSize: Int) {
self.maxSize = maxSize
}
func get(key: Key) -> Value? {
guard let node = dict[key] else { return nil }
moveToHead(node)
return node.value
}
func put(key: Key, value: Value) {
if let node = dict[key] {
node.value = value
moveToHead(node)
return
}
if dict.count >= maxSize {
tail.map { removeNode($0) }
}
let node = Node(value: value)
dict[key] = node
addToHead(node)
}
}
Ebben a Swift implementációban a Node egy belső osztály value, next és prev mezőkkel. A moveToHead metódus leválasztja a csomópontot az aktuális pozícióról, és a lista elejére szúrja be. Túlcsorduláskor a tail — a legrégebben nem használt elem — törlődik. Gyártási verzióhoz ajánlott a szálbiztonság hozzáadása NSLock vagy DispatchQueue segítségével.
Gyakran Ismételt Kérdések
A HashMap nem rendelkezik méretkorlátozási mechanizmussal — végtelenül nő, amíg a memória el nem fogy. LRU Cache egy kiszorítási politikát ad hozzá (a legrégebben nem használt elemek eltávolítása) a határ elérésekor, ami szükséges az OutOfMemoryError megelőzéséhez korlátozott erőforrású mobilalkalmazásokban.
A Google azt javasolja, hogy a képek gyorsítótárához az alkalmazás rendelkezésre álló memóriájának 1/8-át különítsük el (Runtime.maxMemory() / 8). Nehéz grafikájú alkalmazásokhoz 1/4-ig megengedett. Vegye figyelembe a lemez gyorsítótárat is (DiskLruCache), amely 2–5-ször több adatot tárolhat a lassabb, de olcsóbb tárolás árán.
LRU a legrégebben nem használt elemet szorítja ki (az utolsó hozzáférés ideje alapján). LFU a legritkábban használt elemet szorítja ki (a hozzáférés gyakorisága alapján). Az LFU jobb az egyenlőtlen hozzáférési gyakoriságú forgatókönyvekhez, de bonyolultabb implementálni és több memóriát fogyaszt a számlálók tárolásához.
Az NSCache nem dokumentálja a saját kiszorítási politikáját, de a gyakorlatban egy hibrid megközelítést használ, amely közel áll az LRU-hoz LFU elemekkel. Az NSCache automatikusan kiszorítja az objektumokat memóriahiány esetén, és támogatja a költséget (cost) a priorizáláshoz. Garantált LRU-hoz azonban jobb saját implementációt használni.
Thrashing — az az állapot, amikor a gyorsítótár folyamatosan szorít ki és tölt be elemeket valódi haszon nélkül. Akkor fordul elő, amikor az alkalmazás munkadathalmaza nagyobb, mint a gyorsítótár mérete, és az adatokhoz való hozzáférés ciklikus. Megoldás — a gyorsítótár méretének növelése, LFU használata vagy az adaptív ARC (Adaptive Replacement Cache) algoritmus alkalmazása.
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is