LRU Cache — คืออะไร, อัลกอริทึมการขับไล่และวิธีการทำงาน

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-06-12 เวลาอ่าน: 8 นาที

LRU Cache (Least Recently Used Cache) คืออัลกอริทึมการแคชที่ขับไล่องค์ประกอบที่ไม่ได้ใช้งานนานที่สุดเมื่อขนาดแคชถึงขีดจำกัด ในการอ่านหรือเขียนแต่ละครั้ง องค์ประกอบจะถูกย้ายไปที่ด้านหน้าของคิว และเมื่อล้น องค์ประกอบจากส่วนท้ายจะถูกลบออก ตามเอกสารประกอบของ Android Developers (2026) LruCache ใน Android ใช้ LinkedHashMap ในโหมด access-order และให้ความซับซ้อน O(1) สำหรับการดำเนินการ get และ put

ประเด็นสำคัญ

  • LRU Cache — อัลกอริทึมการแคชที่ขับไล่องค์ประกอบตามหลักการ “ไม่ได้ใช้ล่าสุด”
  • ความซับซ้อน ของการดำเนินการ get และ put คือ O(1) เมื่อใช้งานด้วย HashMap + Doubly Linked List
  • Access-order — แต่ละครั้งที่เข้าถึง องค์ประกอบจะถูกย้ายไปด้านหน้า การขับไล่เกิดขึ้นจากด้านท้าย
  • การประยุกต์ใช้ — การแคชรูปภาพ คำขอเครือข่าย ผลลัพธ์การคำนวณ และข้อมูลฐานข้อมูล
  • Android LruCache — การใช้งานพร้อมใช้ในแพ็คเกจ android.util, thread-safe พร้อมรองรับ maxSize

LRU Cache คืออะไร?

LRU Cache (Least Recently Used Cache) คือโครงสร้างข้อมูลขนาดคงที่ที่เก็บองค์ประกอบจำนวนจำกัดและลบองค์ประกอบที่ถูกเข้าถึงน้อยที่สุดโดยอัตโนมัติ เมื่อแอปพลิเคชันร้องขอองค์ประกอบ มันจะย้ายไปยังส่วน “ใหม่” ของแคช ในขณะที่องค์ประกอบที่ไม่ได้ใช้งานเป็นเวลานานจะเลื่อนไปทางด้านท้ายและถูกลบออกเมื่อถึงขีดจำกัด

ชื่อ “Least Recently Used” อธิบายนโยบายการขับไล่: องค์ประกอบที่ไม่ได้ใช้งานนานที่สุดในบรรดาองค์ประกอบที่เก็บไว้ทั้งหมดจะถูกลบออก สิ่งนี้ขึ้นอยู่กับสมมติฐานของ locality of reference (การอ้างอิงเฉพาะที่) — ข้อมูลที่ร้องขอเมื่อเร็ว ๆ นี้มีแนวโน้มสูงที่จะจำเป็นอีกครั้ง นี่คือเหตุผลที่ LRU ถือเป็นหนึ่งในกลยุทธ์การแคชที่มีประสิทธิภาพที่สุดสำหรับแอปพลิเคชันส่วนใหญ่

การใช้งาน LRU Cache แบบคลาสสิกต้องใช้โครงสร้างข้อมูลสองแบบ: ตารางแฮชสำหรับการเข้าถึง O(1) ไปยังองค์ประกอบใด ๆ ด้วยคีย์ และรายการเชื่อมโยงสองทิศทางสำหรับติดตามลำดับการใช้งาน ตารางแฮชเก็บ การอ้างอิง ไปยังโหนดของรายการ และรายการรักษาลำดับจากองค์ประกอบใหม่สุด (หัว) ไปยังเก่าที่สุด (หาง)

การดำเนินการพื้นฐานของ LRU Cache

การดำเนินการ get(key) ตรวจสอบว่ามีคีย์อยู่ในตารางแฮชหรือไม่ หากพบองค์ประกอบ มันจะถูกย้ายไปที่หัวของรายการ (กลายเป็นใหม่ที่สุด) และค่าของมันจะถูกส่งคืน หากไม่พบ จะส่งคืน null หรือโยนข้อยกเว้น การดำเนินการ put(key, value) แทรกองค์ประกอบใหม่: หากคีย์มีอยู่แล้ว ค่าจะถูกอัปเดตและองค์ประกอบจะถูกย้ายไปที่หัว หากแคชเต็ม องค์ประกอบที่หางจะถูกลบออกก่อนการแทรก การดำเนินการทั้งหมดดำเนินการในเวลาคงที่ O(1)

LRU Cache ทำงานอย่างไร

อัลกอริทึม LRU Cache ขึ้นอยู่กับสองหลักการ: การนับการเข้าถึงตามลำดับเวลา และกลไกการขับไล่เมื่อล้น องค์ประกอบแต่ละรายการถูกเก็บไว้ในโหนดของรายการเชื่อมโยงสองทิศทาง และตัวชี้ไปยังโหนดเหล่านี้จะถูกเก็บไว้ในตารางแฮช ในการเข้าถึงแต่ละครั้ง องค์ประกอบจะถูกแยกออกจากตำแหน่งปัจจุบันและแทรกที่หัวของรายการ

เมื่อขนาดแคชถึงค่าสูงสุด (maxSize) และมีคำขอแทรกองค์ประกอบใหม่ อัลกอริทึมจะลบองค์ประกอบที่หางของรายการเชื่อมโยงสองทิศทาง — นี่คือองค์ประกอบที่ไม่ได้ใช้ล่าสุด หลังจากการลบ พื้นที่ว่างสำหรับองค์ประกอบใหม่จะถูกปลดปล่อย ซึ่งจะถูกแทรกที่หัวของรายการ ตารางแฮชจะถูกอัปเดตตามนั้น: คีย์เก่าถูกลบออก คีย์ใหม่ถูกเพิ่ม

ลักษณะเฉพาะของ LRU คือความไวต่อ รูปแบบการเข้าถึง ที่มีการทำซ้ำแบบเป็นรอบ หากแอปพลิเคชันเข้าถึงชุดข้อมูลที่ใหญ่กว่าขนาดแคชเป็นระยะ LRU อาจประสบกับ thrashing — การแทนที่องค์ประกอบบ่อยครั้งที่คำขอใหม่แต่ละครั้งขับไล่คำขอก่อนหน้า ในสถานการณ์เช่นนี้ LFU (Least Frequently Used) หรืออัลกอริทึมแบบปรับตัวอาจมีประสิทธิภาพมากกว่า

ขนาดแคชและเมตริก

การเลือกขนาด LRU Cache เป็นการแลกเปลี่ยนระหว่างการใช้หน่วยความจำและอัตราการเข้าถึง (เปอร์เซ็นต์ของการเข้าถึงที่สำเร็จ) ค่าทั่วไปสำหรับแอปพลิเคชันมือถือ: 10–20% ของหน่วยความจำที่มีสำหรับแคชรูปภาพ และ 50–200 รายการสำหรับแคชการตอบสนองเครือข่าย อัตราการเข้าถึง 80–95% ถือว่าดี โดยที่แคชทำให้ต้นทุนหน่วยความจำคุ้มค่า สำหรับการตรวจสอบ จะใช้ตัวนับ hitCount และ missCount ซึ่งมีอยู่ในการใช้งาน LruCache ใน Android

การใช้งาน LRU Cache: HashMap + Doubly Linked List

การใช้งานมาตรฐานของ LRU Cache ใช้การรวมกันของตารางแฮชและรายการเชื่อมโยงสองทิศทาง ตารางแฮชให้การเข้าถึง O(1) ไปยังโหนดใด ๆ ด้วยคีย์ ในขณะที่รายการเชื่อมโยงสองทิศทางช่วยให้ย้ายโหนดไปที่หัวและลบออกจากหางใน O(1) สิ่งสำคัญคือรายการเป็นแบบเชื่อมโยงสองทิศทาง: ซึ่งช่วยให้สามารถแยกโหนดออกจากกลางรายการได้โดยไม่ต้องวนซ้ำองค์ประกอบทั้งหมด

kotlin
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)
    }
}

ในการใช้งาน แต่ละโหนด (Node) เก็บค่าและการอ้างอิงไปยังโหนดก่อนหน้าและถัดไป โหนด sentinel head และ tail ช่วยลดความซับซ้อนของกรณีขอบ — ไม่จำเป็นต้องตรวจสอบ null เมื่อแทรกและลบ เมธอด get ย้ายโหนดที่พบไปที่หัว และ put ลบองค์ประกอบที่หางเมื่อล้น เมธอดแยกต่างหาก removeKeyByValue ค้นหาคีย์ในตารางแฮชโดยการอ้างอิงโหนดและลบออก

การใช้งาน LruCache ในตัวใน Android

Android SDK มีคลาส LruCache พร้อมใช้ในแพ็คเกจ android.util ซึ่งใช้อัลกอริทึม LRU โดยใช้ LinkedHashMap ในโหมด access-order คลาสนี้เป็น thread-safe รองรับการนับ hit/miss และให้ callback entryRemoved สำหรับการล้างทรัพยากรเมื่อขับไล่องค์ประกอบ ขนาดแคชถูกกำหนดในหน่วยตามอำเภอใจ (ไบต์ จำนวนองค์ประกอบ) — เพียงแค่แทนที่เมธอด sizeOf

LRU Cache เทียบกับ FIFO และ LIFO

ทั้งสามอัลกอริทึม — LRU, FIFO และ LIFO — แก้ปัญหาเดียวกัน: จำกัดการใช้หน่วยความจำโดยการขับไล่องค์ประกอบเมื่อล้น อย่างไรก็ตาม พวกมันใช้เกณฑ์ที่แตกต่างกันโดยพื้นฐานในการเลือกเหยื่อ ซึ่งกำหนดประสิทธิภาพในสถานการณ์ต่าง ๆ

พารามิเตอร์LRUFIFOLIFO
เกณฑ์การขับไล่ไม่ได้ใช้ล่าสุดเพิ่มครั้งแรกเพิ่มครั้งสุดท้าย
โครงสร้างข้อมูลHashMap + รายการเชื่อมโยงสองทิศทางคิว (Queue)สแต็ก (Stack)
ความซับซ้อน get/putO(1)O(1)O(1)
ความทนทานต่อรูปแบบสูงปานกลางต่ำ
การใช้งานทั่วไปแคชรูปภาพและข้อมูลบัฟเฟอร์สตรีมเลิกทำ (undo)

FIFO ขับไล่องค์ประกอบที่เก่าที่สุดตามเวลาเพิ่ม โดยไม่คำนึงถึงความถี่ในการเข้าถึง สิ่งนี้อาจไม่มีประสิทธิภาพหากองค์ประกอบเก่ายังคงเกี่ยวข้อง LRU หลีกเลี่ยงข้อเสียเปรียบนี้โดยพิจารณารูปแบบการเข้าถึง LIFO ขับไล่องค์ประกอบที่เพิ่มล่าสุด — มีประโยชน์สำหรับสถานการณ์การเลิกทำ แต่ไม่เหมาะสำหรับการแคช เนื่องจากข้อมูลใหม่มักจำเป็นมากกว่าข้อมูลเก่า LRU ถือเป็นความสมดุลที่เหมาะสมที่สุดระหว่างความซับซ้อนในการใช้งานและอัตราการเข้าถึงสำหรับแอปพลิเคชันส่วนใหญ่

ตัวอย่างโค้ด LRU Cache

ลองพิจารณาการใช้คลาส LruCache ในตัวจาก Android SDK สำหรับการแคชรูปภาพที่ดาวน์โหลด ตัวอย่างแสดงการเริ่มต้นแคชที่ 1/8 ของหน่วยความจำที่มีของแอปพลิเคชัน ซึ่งเป็นคำแนะนำมาตรฐานของ Google สำหรับการแคชรูปภาพ

kotlin
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)
    }
}

เมธอด sizeOf ส่งคืนขนาดองค์ประกอบในหน่วยเดียวกับที่ระบุ cacheSize ที่นี่ใช้ขนาด Bitmap ในหน่วยกิโลไบต์ (rowBytes × height / 1024) เมื่อผลรวม sizeOf ขององค์ประกอบทั้งหมดเกิน cacheSize LruCache จะขับไล่ Bitmap ที่ไม่ได้ใช้ล่าสุดโดยอัตโนมัติ callback entryRemoved สามารถใช้เพื่อเรียก bitmap.recycle() — ปลดปล่อยหน่วยความจำก่อนการขับไล่

การใช้งาน LRU Cache ใน Swift

iOS ไม่มีคลาส LRU Cache ในตัว แต่สามารถใช้งานได้ง่ายโดยใช้ NSCache (ซึ่งใช้นโยบายการขับไล่ที่คล้ายกันแต่ไม่ได้บันทึกไว้) หรือผ่านการใช้งานแบบกำหนดเองโดยใช้ Dictionary + รายการเชื่อมโยงสองทิศทาง ดังที่แสดงด้านล่าง

swift
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)
    }
}

ในการใช้งาน Swift นี้ Node เป็นคลาสภายในที่มีฟิลด์ value, next และ prev เมธอด moveToHead แยกโหนดออกจากตำแหน่งปัจจุบันและแทรกที่หัวของรายการ เมื่อล้น หาง — องค์ประกอบที่ไม่ได้ใช้ล่าสุด — จะถูกลบออก สำหรับการใช้งานจริง แนะนำให้เพิ่มความปลอดภัยของเธรดผ่าน NSLock หรือคิว DispatchQueue

คำถามที่พบบ่อย

LRU Cache แตกต่างจาก HashMap ทั่วไปอย่างไร?

HashMap ไม่มีกลไกการจำกัดขนาด — มันจะเติบโตอย่างไม่มีที่สิ้นสุดจนกว่าหน่วยความจำจะหมด LRU Cache เพิ่มนโยบายการขับไล่ (การลบองค์ประกอบที่ไม่ได้ใช้ล่าสุด) เมื่อถึงขีดจำกัด ซึ่งจำเป็นเพื่อป้องกัน OutOfMemoryError ในแอปพลิเคชันมือถือที่มีทรัพยากรจำกัด

วิธีเลือกขนาด LRU Cache สำหรับรูปภาพ?

Google แนะนำให้จัดสรร 1/8 ของหน่วยความจำที่มี สำหรับแคชรูปภาพ (Runtime.maxMemory() / 8) สำหรับแอปพลิเคชันที่มีกราฟิกหนัก สูงสุด 1/4 เป็นที่ยอมรับได้ พิจารณาแคชบนดิสก์ (DiskLruCache) ซึ่งสามารถเก็บข้อมูลได้มากกว่า 2–5 เท่าด้วยพื้นที่จัดเก็บที่ช้ากว่าแต่ถูกกว่า

ความแตกต่างระหว่าง LRU และ LFU Cache คืออะไร?

LRU ขับไล่องค์ประกอบที่ไม่ได้ใช้งานนานที่สุด (ตามเวลาการเข้าถึงล่าสุด) LFU ขับไล่องค์ประกอบที่ถูกใช้งานน้อยที่สุด (ตามความถี่ในการเข้าถึง) LFU เหมาะสำหรับสถานการณ์ที่มีความถี่ในการเข้าถึงไม่เท่ากัน แต่ซับซ้อนกว่าในการใช้งานและใช้หน่วยความจำมากขึ้นในการเก็บตัวนับ

NSCache ใน iOS รองรับนโยบาย LRU หรือไม่?

NSCache ไม่ได้บันทึกนโยบายการขับไล่ของมัน แต่ในทางปฏิบัติใช้วิธีการแบบไฮบริดที่ใกล้เคียงกับ LRU โดยมีองค์ประกอบ LFU บางส่วน NSCache ขับไล่วัตถุโดยอัตโนมัติเมื่อหน่วยความจำต่ำและรองรับการจัดลำดับความสำคัญตามต้นทุน อย่างไรก็ตาม สำหรับพฤติกรรม LRU ที่รับประกัน แนะนำให้ใช้งานแบบกำหนดเอง

thrashing ในบริบทของ LRU Cache คืออะไร?

Thrashing คือสถานะที่แคชขับไล่และโหลดองค์ประกอบอย่างต่อเนื่องโดยไม่มีประโยชน์จริง เกิดขึ้นเมื่อชุดข้อมูลทำงานของแอปพลิเคชันใหญ่กว่าขนาดแคชและการเข้าถึงข้อมูลเป็นแบบวนรอบ วิธีแก้ไขรวมถึงการเพิ่มขนาดแคช การใช้ LFU หรือการประยุกต์ใช้อัลกอริทึมแบบปรับตัว ARC (Adaptive Replacement Cache)

สรุป

  • LRU Cache — อัลกอริทึมการแคชที่ขับไล่องค์ประกอบที่ไม่ได้ใช้ล่าสุดเมื่อล้น
  • ความซับซ้อน O(1) สำหรับ get และ put ทำได้โดยการรวมกันของ HashMap และรายการเชื่อมโยงสองทิศทาง
  • Access-order — แต่ละคำขอย้ายองค์ประกอบไปด้านหน้า การขับไล่เกิดขึ้นจากด้านท้ายของรายการ
  • หลักการเฉพาะที่ — ข้อมูลที่ร้องขอเมื่อเร็ว ๆ นี้มีแนวโน้มสูงที่จะจำเป็นอีกครั้ง
  • อัตราการเข้าถึง 80–95% ถือว่าดีสำหรับสถานการณ์การแคชส่วนใหญ่
  • LruCache ใน Android — การใช้งาน thread-safe พร้อมใช้พร้อมการนับ hit/miss และ callback
  • ใช้ LRU สำหรับการแคชรูปภาพ ข้อมูลเครือข่าย และผลลัพธ์การคำนวณในแอปพลิเคชันมือถือ

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม