Starvation ในแอปพลิเคชันมือถือ — สาระสำคัญ สาเหตุ และวิธีป้องกันภาวะขาดแคลนเธรด

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

Starvation (ภาวะขาดแคลนเธรด) คือสถานการณ์ที่เธรดไม่สามารถเข้าถึงทรัพยากรที่จำเป็นสำหรับการทำงานต่อได้ แม้ว่าจะพร้อมทำงานก็ตาม ตามข้อมูลจาก Baeldung (Java Thread Starvation, 2024) ภาวะขาดแคลนเกิดจากการจัดลำดับที่ไม่เป็นธรรม ซึ่งเธรดที่มีลำดับความสำคัญต่ำถูกเลื่อนออกไปอย่างต่อเนื่องเพื่อให้เธรดที่มีลำดับความสำคัญสูงกว่า ต่างจาก Deadlock ตรงที่ Starvation ไม่ได้บล็อกเธรด — มันยังคงอยู่ในสถานะ RUNNABLE แต่ไม่ได้รับเวลา CPU เลย

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

  • Starvation — สถานการณ์ที่เธรดไม่สามารถเข้าถึงทรัพยากรได้แม้จะพร้อมทำงาน
  • ต่างจาก Deadlock เธรดที่ขาดแคลนยังคงอยู่ในสถานะ RUNNABLE — ไม่ได้ถูกบล็อก แต่ไม่คืบหน้า
  • การจัดลำดับที่ไม่เป็นธรรม (เช่น การซิงโครไนซ์ผ่าน synchronized) — สาเหตุหลักของ Starvation บน JVM
  • Fair Lock (ReentrantLock(true)) รับประกันลำดับ FIFO ที่เป็นธรรมในการรับล็อค
  • Thread Priority ในการพัฒนาแอปมือถือแนะนำไม่ให้เปลี่ยนแปลง — Android Runtime จัดการลำดับความสำคัญเอง

Starvation คืออะไร?

Starvation (ภาวะขาดแคลนเธรด) คือปัญหาการเขียนโปรแกรมแบบหลายเธรดที่เธรดไม่สามารถเข้าถึงทรัพยากรที่จำเป็นสำหรับการทำงานให้สำเร็จได้ แม้ว่าทรัพยากรจะไม่ได้ถูกล็อคถาวรโดยเธรดอื่นก็ตาม เธรดอยู่ในสถานะ RUNNABLE แต่ตัวจัดลำดับหรือกลไกการซิงโครไนซ์เลื่อนการทำงานของมันอย่างเป็นระบบเพื่อให้เธรดอื่นทำงานก่อน

ในการพัฒนาแอปมือถือ Starvation แสดงออกเป็น การทำงานของงานที่ไม่เท่าเทียมกัน: บางงานทำงานทันที ในขณะที่บางงานประสบกับความล่าช้าอย่างรุนแรง ตัวอย่างเช่น เธรดซิงโครไนซ์ข้อมูลพื้นหลังอาจไม่สามารถเข้าถึงฐานข้อมูลได้เลยหากเธรด UI และตัวจัดการแอนิเมชันแซงหน้ามันตลอดเวลา ตาม Android Developer Blog (Performance Matters, 2023) ประมาณ 12% ของเฟรมที่พลาด (jank) บน Android เกิดจาก Starvation ของงานพื้นหลังที่การเรนเดอร์ต้องพึ่งพา

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

สาเหตุของภาวะขาดแคลนเธรด

ล็อคที่ไม่เป็นธรรม (Non-Fair Locks)

synchronized ใน Java และ Kotlin เป็นตัวอย่างคลาสสิกของกลไกที่ไม่เป็นธรรม ภายใต้การแข่งขันสูง JVM อาจให้ล็อคแก่เธรดที่ทำงานอยู่เดียวกันอย่างต่อเนื่อง ในขณะที่เธรดอื่นแพ้การแข่งขันตลอดเวลา นี่ไม่ใช่บั๊กของ JVM แต่เป็นข้อแลกเปลี่ยนในการออกแบบ: ล็อคที่ไม่เป็นธรรมให้ปริมาณงานที่สูงกว่าโดยแลกกับความเป็นธรรมในการเข้าถึง สำหรับแอปพลิเคชันมือถือที่มี 4–8 เธรด ปัญหานี้มีความเกี่ยวข้องเป็นพิเศษ

การใช้ลำดับความสำคัญที่ไม่ถูกต้อง

การตั้งค่า ลำดับความสำคัญของเธรดที่แตกต่างกัน อาจนำไปสู่ Starvation ของเธรดที่มีลำดับความสำคัญต่ำ ใน Android Runtime ตัวจัดลำดับ CFS (Completely Fair Scheduler) ของ Linux จัดสรรเวลา CPU ตามสัดส่วนของลำดับความสำคัญ และหากเธรดที่มีลำดับความสำคัญสูงทำงานตลอดเวลา เธรดที่มีลำดับความสำคัญต่ำอาจไม่ได้รับเวลา CPU เลย Google ไม่แนะนำอย่างยิ่งให้เปลี่ยนลำดับความสำคัญของเธรดใน Android — ระบบจัดการเอง

ส่วนวิกฤตที่ยาวนาน

หากเธรดล็อค นานเกินไป (ดำเนินการคำนวณหนัก คำขอเครือข่าย หรือการดำเนินการไฟล์ภายในบล็อก synchronized) เธรดอื่นที่รอล็อคนั้นจะขาดแคลน นี่เป็นอันตรายโดยเฉพาะใน Android ซึ่งการดำเนินการยาวนานบนเธรด UI ทำให้เกิด ANR และการย้ายไปยังเธรดพื้นหลังโดยไม่ปรับส่วนวิกฤตให้เหมาะสมเพียงแค่ย้ายปัญหา Starvation ไปยังเธรดผู้ทำงาน

ตัวอย่าง Starvation ในโค้ด Kotlin

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

kotlin
class SharedResource {
    private val lock = Any()

    fun criticalSection(id: String) {
        synchronized(lock) {
            println("$id ได้รับการเข้าถึง")
            Thread.sleep(10)  // จำลองการทำงาน
        }
    }
}

fun main() {
    val resource = SharedResource()

    // เธรดที่มีลำดับความสำคัญสูง — ทำงานตลอดเวลา
    val highPriority = Thread {
        while (true) {
            resource.criticalSection("High")
        }
    }

    // เธรดที่มีลำดับความสำคัญต่ำ — อาจไม่ได้รับการเข้าถึงเลย
    val lowPriority = Thread {
        while (true) {
            resource.criticalSection("Low")
        }
    }

    highPriority.start()
    lowPriority.start()
    // "Low" อาจไม่พิมพ์ข้อความเลย — Starvation!
}

ในตัวอย่างนี้ เธรด highPriority ล็อคตลอดเวลาและปล่อยเพียง 10 ms เนื่องจากลักษณะที่ไม่เป็นธรรมของ synchronized ตัวจัดลำดับ JVM มีแนวโน้มสูงที่จะให้ล็อคอีกครั้งกับเธรดเดียวกับที่เพิ่งปล่อย — เธรดที่มีลำดับความสำคัญต่ำขาดแคลน วิธีแก้คือใช้ ReentrantLock(true) พร้อมแฟล็ก fair ซึ่งรับประกันลำดับการรอ FIFO

เวอร์ชันที่แก้ไขด้วยล็อคที่เป็นธรรมช่วยให้การเข้าถึงทรัพยากรเป็นธรรม

kotlin
class FairSharedResource {
    private val fairLock = ReentrantLock(true)  // fair = true

    fun criticalSection(id: String) {
        fairLock.lock()
        try {
            println("$id ได้รับการเข้าถึง (fair)")
            Thread.sleep(10)
        } finally {
            fairLock.unlock()
        }
    }
}

Starvation vs Deadlock vs Livelock

ปัญหาหลายเธรดคลาสสิกสามอย่าง — Starvation, Deadlock และ Livelock — มักถูกจัดกลุ่มร่วมกัน แต่กลไกและวิธีแก้ต่างกัน Starvation — เธรดพร้อมแต่ไม่สามารถรับทรัพยากรได้ Deadlock — เธรดถูกบล็อกโดยการรอแบบวน Livelock — เธรดทำงานอยู่แต่ไม่คืบหน้า

พารามิเตอร์StarvationDeadlockLivelock
สถานะเธรดRUNNABLEBLOCKEDRUNNABLE
ความคืบหน้าไม่มีไม่มีไม่มี (แม้ทำงานอยู่)
การใช้ CPUต่ำน้อยที่สุดสูง (ถึง 100%)
สาเหตุการจัดลำดับที่ไม่เป็นธรรมการรอแบบวนการตอบสนองต่อข้อขัดแย้งเหมือนกัน
การแก้ไขหลักFair Lock, ทำให้ส่วนวิกฤตสั้นลงลำดับชั้นล็อคขีดจำกัดการลองใหม่, exponential backoff

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

วิธีตรวจจับ Starvation

Thread Dump ที่ถ่ายซ้ำๆ ในช่วงเวลาสั้นๆ — วิธีการพื้นฐานในการตรวจจับภาวะขาดแคลน หากเธรดอยู่ในสถานะ RUNNABLE อย่างต่อเนื่องแต่สแต็กการเรียกไม่เปลี่ยนแปลงในหลายๆ ดัมพ์ — นี่คือสัญญาณคลาสสิกของ Starvation ใน Android Studio ใช้ Android Profiler ด้วยการบันทึกสถานะเธรดตามเวลา

การตรวจจับอัตโนมัติเป็นไปได้ผ่าน การตรวจสอบเวลาในการทำงาน ของงาน หากงานที่มีเวลาในการทำงานที่คาดเดาได้ (เช่น 50 ms) ใช้เวลา 5 วินาทีขึ้นไป — มีความเป็นไปได้สูงที่จะเกิด Starvation ในแอปพลิเคชันมือถือ Firebase Performance Monitoring ช่วยให้คุณตั้งค่า trace ที่กำหนดเองสำหรับส่วนวิกฤตและรับการแจ้งเตือนเมื่อเกินเกณฑ์

ในการวินิจฉัย Starvation ที่เกิดจากบล็อก synchronized ให้ใช้ Java Flight Recorder (JFR) (ใช้ได้บน Android ผ่าน OpenJDK API) หรือ Async Profiler เครื่องมือเหล่านี้แสดงว่ามอนิเตอร์ใดมีเวลารอนานที่สุดและเธรดใดแข่งขันกันเพื่อแต่ละมอนิเตอร์ ข้อมูล JFR รวมเข้ากับ IntelliJ IDEA Ultimate ผ่านโปรไฟล์เลอร์ในตัว

วิธีป้องกันภาวะขาดแคลนเธรด

Fair Lock (ReentrantLock พร้อมแฟล็ก true)

ReentrantLock(true) รับประกันว่าเธรดได้รับล็อคตามลำดับ FIFO ต่างจาก synchronized ตรงที่ล็อคที่เป็นธรรมไม่อนุญาตให้เธรดที่เพิ่งปล่อยล็อคล็อคซ้ำทันที สิ่งนี้กำจัด Starvation อย่างสมบูรณ์ แม้ว่าจะลดปริมาณงานโดยรวม 10–20% เนื่องจากค่าใช้จ่ายในการดูแลคิว

โครงสร้างอะตอมมิกแบบไม่มีล็อค

โครงสร้างข้อมูลแบบไม่มีล็อค (ConcurrentHashMap, AtomicReference, LongAdder) กำจัด Starvation โดยนิยาม เพราะไม่มีล็อคที่เธรดเดียวสามารถถือครองได้ การดำเนินการทั้งหมดใช้คำสั่ง CAS ของ CPU ซึ่งรับประกันว่าเธรดอย่างน้อยหนึ่งเธรดจะคืบหน้าในจำนวนขั้นตอนที่จำกัด สำหรับการพัฒนาแอปมือถือ ให้เลือกใช้ ConcurrentLinkedQueue สำหรับคิวงาน

ส่วนวิกฤตที่สั้น

การลดเวลาในการถือล็อค เป็นวิธีสากลในการลดความเสี่ยงของ Starvation ย้ายการดำเนินการหนัก (เครือข่าย I/O ดิสก์ การคำนวณซับซ้อน) ออกจากบล็อก synchronized ใช้ ReadWriteLock สำหรับสถานการณ์ที่ผู้อ่านไม่ควรขาดแคลนเนื่องจากผู้เขียนที่หายาก ไลบรารี Kotlin Coroutines มี Mutex พร้อมกลไกการระงับที่ไม่บล็อกเธรด OS

ตัวแปรเงื่อนไขและสัญญาณ

Condition.await() และ signal() ควรใช้ด้วยความระมัดระวัง: เธรดที่รอใน Condition ตื่นขึ้นพร้อมกับเธรดอื่น (spurious wakeup) และทั้งหมดแข่งขันกันเพื่อล็อค หากเธรดกลับไปรอทันทีหลังจาก await ในขณะที่เธรดอื่นได้รับล็อค เธรดที่ขาดแคลนอาจตื่นและหลับไปอย่างไม่สิ้นสุด ตรวจสอบเงื่อนไขในลูป while เสมอ แทนที่จะใช้ if เพื่อให้แน่ใจว่ามีการตรวจสอบซ้ำ

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

ความแตกต่างระหว่าง Starvation และ Priority Inversion คืออะไร?

Priority Inversion (การกลับลำดับความสำคัญ) คือสถานการณ์ที่เธรดที่มีลำดับความสำคัญต่ำถือล็อคที่เธรดที่มีลำดับความสำคัญสูงต้องการ ผลลัพธ์คือเธรดที่มีลำดับความสำคัญสูงรอเธรดที่มีลำดับความสำคัญต่ำ — ลำดับความสำคัญกลับกัน Starvation เป็นปัญหาที่กว้างกว่า: เธรดไม่สามารถรับทรัพยากรได้โดยไม่ขึ้นกับลำดับความสำคัญ เนื่องจากการจัดลำดับที่ไม่เป็นธรรมหรือส่วนวิกฤตที่ยาวนาน

Starvation สามารถเกิดขึ้นในแอปพลิเคชันแบบเธรดเดียวได้หรือไม่?

ไม่ Starvation เป็นปัญหาของหลายเธรด โค้ดแบบเธรดเดียวไม่มีการแข่งขันทรัพยากรหรือการจัดลำดับเธรด อย่างไรก็ตาม Starvation สามารถเกิดขึ้นในโค้ดอะซิงโครนัสแบบเธรดเดียว (เช่น อีเวนต์ลูปของ JavaScript) หากไมโครงานหนึ่งเลื่อนการทำงานของงานอื่นอย่างไม่มีกำหนดผ่าน setTimeout โดยมีความล่าช้าเป็นศูนย์

โมเดลหน่วยความจำ Java เกี่ยวข้องกับ Starvation อย่างไร?

JMM (Java Memory Model) กำหนดกฎสำหรับการมองเห็นการเปลี่ยนแปลงระหว่างเธรด แต่ไม่รับประกันการจัดลำดับที่เป็นธรรม synchronized ตาม JMM รับประกันความสอดคล้องตามลำดับ (sequential consistency) — ความถูกต้องพื้นฐาน — แต่ไม่ได้ป้องกัน Starvation ความเป็นธรรมต้องใช้กลไกเพิ่มเติมที่ไม่ได้ระบุใน JMM

Starvation ในเธรด UI ของ Android คืออะไร?

เธรด UI (Main Thread) ไม่สามารถขาดแคลนในความหมายดั้งเดิมได้เพราะมีลำดับความสำคัญสูงที่สุด อย่างไรก็ตาม Starvation เกิดขึ้นเมื่อเธรด UI รอผลลัพธ์จากเธรดพื้นหลังที่ขาดแคลน สถานการณ์ทั่วไป: AsyncTask หรือโครูทีนโหลดข้อมูล แต่ไม่สามารถเข้าถึงฐานข้อมูลได้เนื่องจากการแข่งขันกับเธรดอื่น และ UI ค้างขณะรอ

จะป้องกัน Starvation ใน Kotlin Coroutines ได้อย่างไร?

ในโครูทีน เพื่อป้องกัน Starvation ให้ใช้ limitedParallelism บน Dispatchers.IO เพื่อหลีกเลี่ยงการหมดเธรด สำหรับการซิงโครไนซ์ ให้ใช้ Mutex จาก kotlinx.coroutines.sync — มันระงับโครูทีนแทนที่จะบล็อกเธรด ซึ่งลดความเสี่ยงในการขาดแคลน หลีกเลี่ยง runBlocking ในโครูทีน เพราะมันอาจยึดเธรดจากพูลและทำให้โครูทีนอื่นขาดแคลน

สรุป

  • Starvation — สถานการณ์ที่เธรดพร้อมทำงานแต่ไม่สามารถรับทรัพยากรได้เนื่องจากการจัดลำดับที่ไม่เป็นธรรม
  • ต่างจาก Deadlock เธรดที่ขาดแคลนยังคงอยู่ในสถานะ RUNNABLE และสามารถทำงานได้เมื่อโหลดลดลง
  • ล็อคที่ไม่เป็นธรรม (synchronized) และการใช้ลำดับความสำคัญที่ไม่ถูกต้องเป็นสาเหตุหลักของ Starvation
  • Fair Lock (ReentrantLock พร้อมแฟล็ก true) รับประกันลำดับการเข้าถึง FIFO และกำจัดภาวะขาดแคลนอย่างสมบูรณ์
  • โครงสร้างแบบไม่มีล็อค (ConcurrentHashMap, AtomicReference) กำจัด Starvation ในระดับสถาปัตยกรรม
  • Thread Dump ด้วยการจับซ้ำๆ และ Java Flight Recorder เป็นวิธีที่มีประสิทธิภาพในการวินิจฉัย Starvation
  • ส่วนวิกฤตที่สั้น และ ReadWriteLock ลดโอกาสขาดแคลนในระบบที่มีโหลดสูง

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

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

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

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