Deadlock ในการพัฒนามือถือ: คืออะไร สาเหตุของการเกิดและวิธีหลีกเลี่ยงการล็อกซึ่งกันและกัน

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

Deadlock (การล็อกซึ่งกันและกัน) เป็นสถานะที่เธรดตั้งแต่สองเธรดขึ้นไปรออย่างไม่มีที่สิ้นสุดเพื่อให้ทรัพยากรที่ถูกยึดครองโดยผู้เข้าร่วมอื่น ๆ ถูกปล่อยออกมา ตาม Oracle Java Tutorials (2024) Deadlock เกิดขึ้นในการรอแบบวงกลมเมื่อแต่ละเธรดถือล็อกที่จำเป็นสำหรับอีกเธรดหนึ่ง หากไม่มีเครื่องมือตรวจจับพิเศษ Deadlock จะหยุดการทำงานของแอปพลิเคชันโดยสมบูรณ์โดยไม่มีข้อผิดพลาดที่มองเห็นได้

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

  • Deadlock — การล็อกซึ่งกันและกันของเธรดที่แต่ละเธรดรอทรัพยากรที่ถูกยึดโดยอีกเธรดหนึ่ง
  • สี่เงื่อนไขของ Coffman (Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait) จำเป็นต่อการเกิด Deadlock
  • Deadlock แตกต่าง จาก Starvation ตรงที่เธรดไม่ได้ถูกบล็อก แต่รออย่างแข็งขันในการพึ่งพาแบบวงจร
  • Thread Dump — เครื่องมือหลักในการตรวจจับ Deadlock ใน JVM และ Android Runtime
  • ลำดับชั้นล็อก และลำดับการครอบครองทรัพยากรที่เป็นหนึ่งเดียว — วิธีหลักในการป้องกันการล็อกซึ่งกันและกัน

Deadlock คืออะไร?

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

ในการพัฒนามือถือ Deadlock มีความสำคัญเป็นพิเศษเพราะมันไม่ทำให้เกิดข้อยกเว้นหรือแครช แอปพลิเคชันเพียงแค่หยุดตอบสนองต่อการกระทำของผู้ใช้ (ANR — Application Not Responding) และทางออกเดียวคือการบังคับสิ้นสุดกระบวนการ ตามข้อมูลของ Google (Android Performance Patterns, 2023) ประมาณ 15% ของรายงาน ANR ใน Google Play Console เกี่ยวข้องกับการล็อกซึ่งกันและกันในเธรดพื้นหลัง

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

เงื่อนไขการเกิด Deadlock

ในปี 1971 Edward G. Coffman ได้กำหนดเงื่อนไขบังคับสี่ประการที่จำเป็นต่อการเกิด Deadlock หากขาดอย่างน้อยหนึ่งข้อ การล็อกซึ่งกันและกันเป็นไปไม่ได้ เงื่อนไขเหล่านี้รู้จักกันในชื่อเงื่อนไขของ Coffman และเป็นพื้นฐานของอัลกอริทึมการป้องกัน Deadlock ทั้งหมด

การกีดกันซึ่งกันและกัน (Mutual Exclusion)

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

ถือและรอ (Hold and Wait)

เธรดถือทรัพยากรที่ครอบครองอยู่แล้วและรอ ครอบครองทรัพยากรอื่น พร้อมกัน หากเธรดสามารถปล่อยทรัพยากรปัจจุบันก่อนขอทรัพยากรถัดไป (ผ่านการล็อกสองเฟส) เงื่อนไข Hold and Wait จะถูกทำลาย ใน Android สิ่งนี้มักปรากฏเมื่อเธรดถือล็อกฐานข้อมูลและพยายามครอบครองล็อก SharedPreferences

ไม่มีการแย่งชิงโดยบังคับ (No Preemption)

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

การรอแบบวงกลม (Circular Wait)

มี ห่วงโซ่ปิดของเธรด ซึ่งแต่ละเธรดรอทรัพยากรที่ถูกถือโดยเธรดถัดไปในห่วงโซ่ ตัวอย่างเช่น เธรด A ถือทรัพยากร 1 และรอทรัพยากร 2 เธรด B ถือทรัพยากร 2 และรอทรัพยากร 1 นี่เป็นเงื่อนไขเดียวที่นักพัฒนาสามารถกำจัดได้ในเชิงสถาปัตยกรรม — ผ่านลำดับชั้นล็อก หากเธรดทั้งหมดครอบครองทรัพยากรในลำดับสากลที่กำหนดอย่างเคร่งครัด วงจรจะเป็นไปไม่ได้ในทางกายภาพ

ในทางปฏิบัติ ในแอปพลิเคชัน Android Deadlock เกิดขึ้นบ่อยที่สุดเนื่องจาก การตัดกันโดยนัย ของล็อกในระดับต่าง ๆ: ล็อกฐานข้อมูล (Room), ล็อก SharedPreferences และล็อกคอลเลกชันในหน่วยความจำ ล็อกแต่ละตัวถูกจัดการโดยส่วนประกอบต่าง ๆ และหากไม่มีโปรโตคอลรวมศูนย์สำหรับลำดับการครอบครอง นักพัฒนาจะสร้างวงจรโดยไม่ตั้งใจ

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

ลองพิจารณาตัวอย่างคลาสสิกของการล็อกซึ่งกันและกัน — สองเธรดครอบครองล็อกในลำดับที่ต่างกัน หากเธรดแรกล็อกทรัพยากร A และพยายามครอบครอง B และเธรดที่สองล็อก B และพยายามครอบครอง A Deadlock จะเกิดขึ้น

kotlin
class DeadlockExample {
    private val lockA = Any()
    private val lockB = Any()

    fun operationA() {
        synchronized(lockA) {
            Thread.sleep(50)  // การจำลองการทำงาน
            synchronized(lockB) {
                println("operationA เสร็จสมบูรณ์")
            }
        }
    }

    fun operationB() {
        synchronized(lockB) {  // ลำดับย้อนกลับ
            Thread.sleep(50)
            synchronized(lockA) {
                println("operationB เสร็จสมบูรณ์")
            }
        }
    }
}

fun main() {
    val ex = DeadlockExample()
    Thread { ex.operationA() }.start()
    Thread { ex.operationB() }.start()
    // แอปพลิเคชันจะค้างตลอดไป — Deadlock!
}

ในตัวอย่างนี้ operationA ครอบครอง lockA และ operationB ครอบครอง lockB จากนั้นแต่ละเธรดพยายามครอบครองล็อกที่สอง — และทั้งคู่รออย่างไม่มีกำหนด โปรแกรมค้างโดยไม่มีข้อความแสดงข้อผิดพลาด วิธีแก้ไขเพียงอย่างเดียวคือรับประกันลำดับการครอบครองล็อกเดียวกันในทุกเมธอด

Deadlock เทียบกับ Starvation เทียบกับ Livelock

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

ลักษณะDeadlockStarvationLivelock
สถานะเธรดถูกบล็อก (BLOCKED)พร้อม (RUNNABLE)ทำงาน (RUNNABLE)
การทำงานไม่ไม่ใช่ แต่ไร้ประโยชน์
สาเหตุการรอแบบวงกลมการจัดตารางเวลาไม่เป็นธรรมการจัดการข้อขัดแย้งไม่ถูกต้อง
การตรวจจับThread Dump, หมดเวลาการติดตามความคืบหน้าตัวนับลองใหม่

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

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

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

Thread Dump เป็นเครื่องมือหลักในการตรวจจับการล็อกซึ่งกันและกันใน JVM และ Android Runtime ระหว่างการดัมพ์ JVM จะวิเคราะห์กราฟการพึ่งพาระหว่างมอนิเตอร์โดยอัตโนมัติและทำเครื่องหมายวงจร Deadlock ใน Android Studio สามารถรับเธรดดัมพ์ได้ผ่าน Android Profiler หรือคำสั่ง kill -3 PID จาก ADB Shell

การตรวจจับ Deadlock อัตโนมัติขณะรันไทม์ถูกนำไปใช้ผ่าน ตัวจับเวลา Watchdog หากเธรดไม่ดำเนินการเสร็จสิ้นภายในเวลาที่กำหนด watchdog จะเริ่มการดัมพ์และส่งรายงานไปยังระบบ Crash Reporting (Firebase Crashlytics, Sentry) ตามข้อมูลของ Sentry (Issue Resolution Report, 2024) การกำหนดค่า watchdog ช่วยลดเวลาการวินิจฉัย Deadlock จากสัปดาห์เหลือเพียงไม่กี่ชั่วโมง

ในขั้นตอนการพัฒนา เครื่องมือวิเคราะห์แบบคงที่ ThreadSafe ของ JetBrains และ Checker Framework พร้อมโมดูล Lock Checker มีประสิทธิภาพ เครื่องมือเหล่านี้วิเคราะห์ลำดับการครอบครองล็อกในระดับซอร์สโค้ดและเตือนเกี่ยวกับวงจรที่อาจเกิดขึ้น นอกจากนี้ แนะนำให้ใช้ Test-Driven Deadlock Detection — การทดสอบความเครียดที่ดำเนินการกับลำดับล็อกต่าง ๆ ในหลายร้อยเธรด

ที่ควรให้ความสนใจเป็นพิเศษคือ Cooperative Deadlock Detection — วิธีการที่เธรดแลกเปลี่ยนข้อมูลเกี่ยวกับล็อกที่ครอบครองผ่านรีจิสทรีส่วนกลาง หากเธรดตรวจพบวงจรที่อาจเกิดขึ้น มันจะปล่อยทรัพยากรทั้งหมดและลองดำเนินการอีกครั้ง วิธีการนี้ใช้ในระบบกระจาย (Apache ZooKeeper, Google Chubby) และกำลังค่อย ๆ ถูกนำมาใช้ในการพัฒนามือถือผ่านไลบรารีเช่น Jetpack Sync

วิธีการป้องกันการล็อกซึ่งกันและกัน

ลำดับชั้นล็อก (Lock Ordering)

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

TryLock กับหมดเวลา

TryLock เป็นวิธีการล็อกที่ไม่บล็อกเธรดอย่างไม่มีกำหนด แต่ส่งคืน false หากไม่สามารถครอบครองล็อกได้ภายในเวลาที่กำหนด ใน Java สิ่งนี้ถูกนำไปใช้ผ่าน ReentrantLock.tryLock(timeout, TimeUnit) ใน Kotlin Coroutines — ผ่าน Mutex.withLock พร้อมหมดเวลา เมื่อล้มเหลว เธรดจะปล่อยทรัพยากรที่ครอบครองทั้งหมดและลองอีกครั้งในภายหลัง

อัลกอริทึมของนายธนาคาร (Banker's Algorithm)

อัลกอริทึมของนายธนาคาร เป็นวิธีการทางทฤษฎีในการป้องกัน Deadlock ที่เสนอโดย Edsger Dijkstra มันจำลองการจัดสรรทรัพยากรเป็นธุรกรรมธนาคาร: ระบบจะไม่จัดสรรทรัพยากรถ้ามันอาจนำไปสู่สถานะที่ไม่ปลอดภัย (deadlock) ในทางปฏิบัติ อัลกอริทึมไม่ค่อยได้ใช้ในการพัฒนามือถือเนื่องจากความยากในการทราบความต้องการสูงสุดของเธรดล่วงหน้า แต่หลักการของมันถูกใช้ในฐานข้อมูล SQLite และระบบไฟล์

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

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

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

Deadlock ใน Kotlin Coroutines แตกต่างจาก Deadlock ในเธรดอย่างไร?

ในคอรูทีน Deadlock เกิดขึ้นในระดับ ฟังก์ชันระงับ (suspend) และไม่บล็อกเธรดของระบบปฏิบัติการ ทำให้สังเกตเห็นได้น้อยกว่า Mutex จาก kotlinx.coroutines เป็นล็อกแบบระงับ (suspending) — มันไม่บล็อกเธรด แต่คอรูทีนจะไม่ทำงาน สำหรับการตรวจจับ ให้ใช้ DebugProbes จากโมดูล kotlinx-coroutines-debug

Deadlock ใน SQLite บน Android คืออะไร?

Deadlock ใน SQLite เกิดขึ้นเมื่อสองการเชื่อมต่อฐานข้อมูลพยายามดำเนินการธุรกรรมในลำดับที่ต่างกัน SQLite ตรวจจับสถานการณ์ดังกล่าวและส่งคืนรหัสข้อผิดพลาด SQLITE_BUSY หรือ SQLITE_LOCKED ใน Android แนะนำให้ใช้ Room พร้อมอินสแตนซ์ฐานข้อมูลเดียวและธุรกรรมผ่าน @Transaction ซึ่งกำจัด Deadlock ระหว่างการเชื่อมต่อ

Android ตรวจจับ Deadlock อย่างไร?

Android Runtime มีตัวตรวจจับ Deadlock ในตัวที่ทำงานเมื่อเกิด ANR (Application Not Responding) ระบบจะวิเคราะห์ Thread Dump ของเธรดทั้งหมดในแอปพลิเคชันและทำเครื่องหมายการล็อกซึ่งกันและกัน ผลลัพธ์สามารถดูได้ที่ /data/anr/traces.txt และใน Google Play Console ส่วน ANR Reports

จะทำอย่างไรถ้าพบ Deadlock ในโปรดักชัน?

ขั้นแรก รับ Thread Dump ของเธรดทั้งหมดในแอปพลิเคชัน วิเคราะห์ว่าแต่ละเธรดถือล็อกอะไรและพยายามครอบครองล็อกอะไร นำตัวจับเวลา Watchdog มาใช้พร้อมการดัมพ์อัตโนมัติเมื่อเกินเวลาที่กำหนด หลังจากแก้ไขแล้ว ให้เพิ่มกฎ lint ThreadSafety ในไปป์ไลน์ CI เพื่อป้องกันการเกิดซ้ำ

สรุป

  • Deadlock — การล็อกซึ่งกันและกันที่เธรดรอทรัพยากรที่ถูกถือโดยกันและกันอย่างไม่มีที่สิ้นสุด
  • สี่เงื่อนไขของ Coffman (Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait) จำเป็นต่อ Deadlock
  • Thread Dump — วิธีการมาตรฐานในการตรวจจับการล็อกซึ่งกันและกันใน JVM และ Android Runtime
  • ลำดับชั้นล็อกที่มีลำดับสากลเป็นหนึ่งเดียว กำจัดเงื่อนไขการรอแบบวงกลมอย่างสมบูรณ์
  • TryLock กับหมดเวลา ป้องกันการรออย่างไม่มีที่สิ้นสุดและช่วยให้เธรดจัดการกับความไม่พร้อมของทรัพยากรได้อย่างถูกต้อง
  • Deadlock เทียบกับ Starvation — ใน Deadlock เธรดถูกบล็อก ใน Starvation เธรดพร้อมทำงานแต่ไม่ได้รับ CPU
  • ตัวจับเวลา Watchdog และเครื่องมือวิเคราะห์แบบคงที่ (ThreadSafe, Checker Framework) — การป้องกันพื้นฐานจาก Deadlock ใน CI/CD

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

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

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

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