Starvation در برنامه‌های موبایل — ماهیت، دلایل بروز و روش‌های جلوگیری از گرسنگی نخ

نویسنده: IT Sectr منتشر شده: 2026-03-18 زمان مطالعه: 10 دقیقه

Starvation (گرسنگی نخ) — وضعیتی است که در آن یک نخ به منبع لازم برای ادامه کار دسترسی پیدا نمی‌کند، اگرچه خود آماده اجراست. به گفته Baeldung (Java Thread Starvation, 2024)، گرسنگی به دلیل برنامه‌ریزی ناعادلانه رخ می‌دهد، زمانی که نخ‌های کم‌اولویت دائماً به نفع نخ‌های با اولویت بالاتر به تعویق می‌افتند. بر خلاف Deadlock، Starvation نخ را مسدود نمی‌کند — در حالت RUNNABLE باقی می‌ماند، اما هرگز زمان پردازنده دریافت نمی‌کند.

نکات اصلی

  • Starvation — وضعیتی که در آن نخ علی‌رغم آمادگی برای اجرا به منبع دسترسی پیدا نمی‌کند
  • بر خلاف Deadlock، نخ در هنگام گرسنگی در حالت RUNNABLE باقی می‌ماند — مسدود نشده، اما پیشرفت نمی‌کند
  • برنامه‌ریزی ناعادلانه (مثلاً همگام‌سازی از طریق synchronized) — علت اصلی Starvation در JVM
  • Fair Lock (ReentrantLock(true)) دسترسی عادلانه به قفل را به ترتیب صف تضمین می‌کند
  • Thread Priority در توسعه موبایل توصیه می‌شود تغییر داده نشود — Android Runtime خود اولویت‌ها را مدیریت می‌کند

Starvation چیست؟

Starvation (گرسنگی نخ) — مشکلی در برنامه‌نویسی چندنخی است که در آن یک نخ نمی‌تواند به منبع لازم برای انجام وظیفه دسترسی پیدا کند، اگرچه منبع برای همیشه توسط نخ دیگری مسدود نشده است. نخ در حالت RUNNABLE قرار دارد، اما برنامه‌ریز یا مکانیزم همگام‌سازی به طور سیستماتیک اجرای آن را به نفع نخ‌های دیگر به تعویق می‌اندازد.

در توسعه موبایل، Starvation به صورت اجرای نابرابر وظایف ظاهر می‌شود: برخی عملیات‌ها فوراً اجرا می‌شوند، برخی دیگر — با تأخیرهای فاجعه‌بار. به عنوان مثال، نخ پس‌زمینه همگام‌سازی داده ممکن است هرگز به پایگاه داده دسترسی پیدا نکند، اگر نخ UI و پردازشگرهای انیمیشن دائماً از آن جلو بزنند. طبق Android Developer Blog (Performance Matters, 2023)، حدود 12٪ موارد فریم‌های از دست رفته (jank) در اندروید ناشی از Starvation وظایف پس‌زمینه‌ای است که رندرینگ به آنها وابسته است.

تفاوت کلیدی Starvation با Deadlock — قابلیت بازگشت. اگر بار سیستم کاهش یابد یا اولویت‌ها دوباره توزیع شوند، نخ گرسنه ممکن است منبع را دریافت کرده و کار را تکمیل کند. اما در شرایط بار بالا و مداوم، Starvation می‌تواند به طور نامحدود ادامه یابد و احساس برنامه قفل شده را ایجاد کند.

دلایل گرسنگی نخ

قفل‌های ناعادلانه (Non-Fair Locks)

synchronized در جاوا و کاتلین — مثال کلاسیک یک مکانیزم ناعادلانه. در رقابت بالا، JVM می‌تواند به طور نامحدود قفل را به همان نخ‌های فعال بدهد، در حالی که نخ‌های دیگر دائماً در مسابقه می‌بازند. این اشکال JVM نیست، بلکه ویژگی پیاده‌سازی است: قفل‌های ناعادلانه توان عملیاتی بالاتری را به قیمت یکنواختی دسترسی فراهم می‌کنند. برای برنامه‌های موبایل با ۴-۸ نخ، این مشکل به ویژه مهم است.

استفاده نادرست از اولویت‌ها

تنظیم اولویت‌های مختلف نخ‌ها می‌تواند منجر به Starvation نخ‌های کم‌اولویت شود. در Android Runtime، برنامه‌ریز CFS (Completely Fair Scheduler) لینوکس زمان پردازنده را متناسب با اولویت‌ها توزیع می‌کند و اگر نخ‌های با اولویت بالا دائماً فعال باشند، نخ‌های کم‌اولویت ممکن است هرگز CPU دریافت نکنند. گوگل قاطعانه تغییر اولویت‌های نخ را در اندروید توصیه نمی‌کند — سیستم خود آنها را مدیریت می‌کند.

بخش‌های بحرانی طولانی

اگر یک نخ قفل را خیلی طولانی نگه دارد (محاسبات سنگین، درخواست‌های شبکه یا عملیات فایل در داخل بلوک synchronized انجام دهد)، نخ‌های دیگر که منتظر این قفل هستند، گرسنه می‌مانند. این به ویژه در اندروید خطرناک است، جایی که عملیات طولانی در نخ 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 دائماً قفل را تصاحب می‌کند و آن را فقط برای ۱۰ میلی‌ثانیه آزاد می‌کند. به دلیل ماهیت ناعادلانه synchronized، برنامه‌ریز JVM به احتمال زیاد قفل را دوباره به همان نخی که به تازگی آن را آزاد کرده می‌دهد — نخ کم‌اولویت گرسنه می‌ماند. راه حل — استفاده از ReentrantLock(true) با پرچم fair که ترتیب صف انتظار را تضمین می‌کند.

نسخه تصحیح شده با fair lock توزیع عادلانه دسترسی به منبع را تضمین می‌کند.

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کمحداقلزیاد (تا ۱۰۰٪)
دلیلبرنامه‌ریزی ناعادلانهانتظار چرخه‌ایواکنش یکسان به تعارض
راه حل اصلیFair Lock، کاهش بخش‌های بحرانیسلسله‌مراتب قفل‌هامحدودیت تلاش مجدد، backoff نمایی

Starvation کمتر بحرانی از Deadlock در نظر گرفته می‌شود، زیرا کشنده نیست — با کاهش بار، نخ گرسنه در نهایت اجرا خواهد شد. اما در شرایط استفاده واقعی از برنامه‌های اندروید، جایی که حافظه و CPU محدود هستند، Starvation می‌تواند دقایقی طول بکشد و UX غیرقابل قبولی ایجاد کند.

چگونه Starvation را تشخیص دهیم

Thread Dump با برداشت‌های مکرر در فواصل کوتاه — روش پایه تشخیص گرسنگی. اگر یک نخ به طور مداوم در حالت RUNNABLE باشد، اما پشته فراخوانی آن در طول چندین dump تغییر نکند — این نشانه کلاسیک Starvation است. در Android Studio از Android Profiler با ضبط وضعیت نخ‌ها در طول زمان برای این منظور استفاده می‌شود.

تشخیص خودکار از طریق نظارت بر زمان اجرای وظایف امکان‌پذیر است. اگر یک وظیفه با زمان اجرای قابل پیش‌بینی (مثلاً ۵۰ میلی‌ثانیه) ۵ ثانیه یا بیشتر اجرا شود — احتمال Starvation بالا است. در برنامه‌های موبایل، Firebase Performance Monitoring امکان پیکربندی ردیابی‌های سفارشی (custom traces) برای بخش‌های بحرانی و دریافت اعلان‌ها در هنگام فراتر رفتن از مقادیر آستانه را فراهم می‌کند.

برای تشخیص Starvation ناشی از بلوک‌های synchronized از Java Flight Recorder (JFR) (موجود در اندروید از طریق OpenJDK API) یا Async Profiler استفاده کنید. این ابزارها نشان می‌دهند کدام مانیتورها بیشترین زمان انتظار را دارند و کدام نخ‌ها برای هر مانیتور رقابت می‌کنند. داده‌های JFR از طریق پروفایلر داخلی با IntelliJ IDEA Ultimate یکپارچه می‌شوند.

روش‌های جلوگیری از گرسنگی نخ

Fair Lock (ReentrantLock با پرچم true)

ReentrantLock(true) تضمین می‌کند که نخ‌ها قفل را به ترتیب صف (FIFO) دریافت می‌کنند. بر خلاف synchronized، fair lock اجازه نمی‌دهد نخی که به تازگی قفل را آزاد کرده بلافاصله دوباره آن را تصاحب کند. این کاملاً Starvation را از بین می‌برد، اگرچه توان عملیاتی کلی را به دلیل سربار نگهداری صف ۱۰-۲۰٪ کاهش می‌دهد.

ساختارهای اتمی بدون قفل

ساختارهای داده Lock-free (ConcurrentHashMap، AtomicReference، LongAdder) ذاتاً Starvation را حذف می‌کنند، زیرا حاوی قفل‌هایی نیستند که توسط یک نخ نگه داشته شوند. همه عملیات‌ها از دستورالعمل‌های CAS پردازنده استفاده می‌کنند که پیشرفت حداقل یک نخ را در تعداد محدودی گام تضمین می‌کند. برای توسعه موبایل، ConcurrentLinkedQueue را برای صف‌های وظایف ترجیح دهید.

بخش‌های بحرانی کوتاه

به حداقل رساندن زمان نگهداری قفل — روشی جهانی برای کاهش خطر Starvation. عملیات سنگین (شبکه، ورودی/خروجی دیسک، محاسبات پیچیده) را خارج از بلوک synchronized قرار دهید. از ReadWriteLock برای سناریوهایی استفاده کنید که خوانندگان نباید به دلیل نویسندگان نادر گرسنه بمانند. کتابخانه Kotlin Coroutines Mutex را با مکانیزم suspending فراهم می‌کند که نخ سیستم‌عامل را مسدود نمی‌کند.

متغیرهای شرطی و سیگنال‌ها

Condition.await() و signal() باید با احتیاط استفاده شوند: نخی که روی Condition منتظر است همراه با نخ‌های دیگر بیدار می‌شود (spurious wakeup) و همه آنها برای قفل رقابت می‌کنند. اگر یک نخ پس از await بلافاصله به انتظار بازگردد و دیگران موفق به تصاحب قفل شوند — نخ گرسنه ممکن است بی‌نهایت بیدار شود و بخوابد. همیشه شرط را در حلقه while بررسی کنید، نه در if، تا بررسی مجدد تضمین شود.

سوالات متداول

تفاوت بین Starvation و Priority Inversion چیست؟

Priority Inversion — وضعیتی است که در آن یک نخ کم‌اولویت قفلی را نگه می‌دارد که برای یک نخ با اولویت بالا لازم است. در نتیجه نخ با اولویت بالا منتظر نخ کم‌اولویت می‌ماند — اولویت‌ها معکوس می‌شوند. Starvation مشکل گسترده‌تری است: نخ بدون توجه به اولویت، به دلیل برنامه‌ریزی ناعادلانه یا بخش‌های بحرانی طولانی، منبع دریافت نمی‌کند.

آیا Starvation می‌تواند در یک برنامه تک‌نخی رخ دهد؟

خیر، Starvation — مشکل چندنخی است. در کد تک‌نخی رقابتی برای منابع و برنامه‌ریزی نخ وجود ندارد. با این حال، Starvation می‌تواند در کد ناهمگام تک‌نخی (مثلاً حلقه رویداد JavaScript) رخ دهد، اگر یک میکرووظیفه به طور نامحدود اجرای دیگران را از طریق setTimeout با تأخیر صفر به تعویق بیندازد.

Java Memory Model چگونه با Starvation مرتبط است؟

JMM (Java Memory Model) قوانین مشاهده‌پذیری تغییرات بین نخ‌ها را تعریف می‌کند، اما برنامه‌ریزی عادلانه را تضمین نمی‌کند. synchronized مطابق با JMM sequential consistency — صحت پایه را تضمین می‌کند، اما از Starvation جلوگیری نمی‌کند. برای عدالت، مکانیزم‌های اضافی خارج از مشخصات JMM مورد نیاز است.

Starvation در نخ UI اندروید چیست؟

نخ UI (Main Thread) نمی‌تواند به معنای کلاسیک گرسنه بماند، زیرا بالاترین اولویت را دارد. با این حال، Starvation زمانی رخ می‌دهد که نخ UI منتظر نتیجه یک نخ پس‌زمینه گرسنه بماند. سناریوی معمول: AsyncTask یا کوروتین داده‌ها را بارگیری می‌کند اما به دلیل رقابت با نخ‌های دیگر نمی‌تواند به پایگاه داده دسترسی پیدا کند و UI در انتظار قفل می‌ماند.

چگونه از Starvation در Kotlin Coroutines جلوگیری کنیم؟

در کوروتین‌ها برای جلوگیری از Starvation از limitedParallelism روی Dispatchers.IO استفاده کنید تا از تخلیه نخ‌ها جلوگیری شود. برای همگام‌سازی از Mutex از kotlinx.coroutines.sync استفاده کنید — کوروتین را معلق می‌کند، نه اینکه نخ را مسدود کند، که خطر گرسنگی را کاهش می‌دهد. از runBlocking در کوروتین‌ها خودداری کنید، زیرا می‌تواند نخ pool را تصاحب کرده و باعث Starvation سایر کوروتین‌ها شود.

خلاصه

  • Starvation — وضعیتی که در آن نخ آماده اجراست اما به دلیل برنامه‌ریزی ناعادلانه منبع دریافت نمی‌کند
  • بر خلاف Deadlock، در گرسنگی نخ در حالت RUNNABLE است و با کاهش بار می‌تواند اجرا شود
  • قفل‌های ناعادلانه (synchronized) و استفاده نادرست از اولویت‌ها — دلایل اصلی Starvation
  • Fair Lock (ReentrantLock با پرچم true) ترتیب دسترسی FIFO را تضمین می‌کند و گرسنگی را کاملاً از بین می‌برد
  • ساختارهای Lock-free (ConcurrentHashMap، AtomicReference) Starvation را در سطح معماری حذف می‌کنند
  • Thread Dump با برداشت‌های مکرر و Java Flight Recorder — روش‌های مؤثر تشخیص Starvation
  • بخش‌های بحرانی کوتاه و ReadWriteLock احتمال گرسنگی را در سیستم‌های پربار کاهش می‌دهند

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید