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) στο Android προκαλούνται από Starvation εργασιών παρασκηνίου από τις οποίες εξαρτάται η απόδοση.

Η βασική διαφορά του Starvation από το Deadlock — αναστρεψιμότητα. Εάν το φορτίο του συστήματος μειωθεί ή οι προτεραιότητες ανακατανεμηθούν, το νήμα που πεινά μπορεί να αποκτήσει τον πόρο και να ολοκληρώσει την εργασία. Ωστόσο, υπό συνθήκες συνεχούς υψηλού φορτίου, το Starvation μπορεί να διαρκέσει απεριόριστα, δημιουργώντας την εντύπωση παγωμένης εφαρμογής.

Αιτίες πείνας νήματος

Άδικα κλειδώματα (Non-Fair Locks)

synchronized σε Java και Kotlin — κλασικό παράδειγμα άδικου μηχανισμού. Σε υψηλό ανταγωνισμό, το JVM μπορεί να δίνει το κλείδωμα επ' αόριστον στα ίδια ενεργά νήματα, ενώ άλλα νήματα χάνουν συνεχώς στον αγώνα. Αυτό δεν είναι σφάλμα του JVM, αλλά χαρακτηριστικό υλοποίησης: τα άδικα κλειδώματα παρέχουν υψηλότερη απόδοση εις βάρος της ομοιομορφίας πρόσβασης. Για εφαρμογές κινητών με 4-8 νήματα, αυτό το πρόβλημα είναι ιδιαίτερα σημαντικό.

Λανθασμένη χρήση προτεραιοτήτων

Η ρύθμιση διαφορετικών προτεραιοτήτων νημάτων μπορεί να οδηγήσει σε Starvation νημάτων χαμηλής προτεραιότητας. Στο Android Runtime, ο προγραμματιστής CFS (Completely Fair Scheduler) του Linux κατανέμει τον χρόνο επεξεργαστή αναλογικά με τις προτεραιότητες, και εάν τα νήματα υψηλής προτεραιότητας είναι συνεχώς ενεργά, τα νήματα χαμηλής προτεραιότητας μπορεί να μην λάβουν ποτέ 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, η οποία εγγυάται τη σειρά στην ουρά αναμονής.

Η διορθωμένη έκδοση με 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ΧαμηλήΕλάχιστηΥψηλή (έως 100%)
ΑιτίαΆδικος προγραμματισμόςΚυκλική αναμονήΊδια αντίδραση σε σύγκρουση
Κύρια λύσηFair Lock, μείωση κρίσιμων ενοτήτωνΙεραρχία κλειδωμάτωνΌριο επαναλήψεων, εκθετική υπαναχώρηση

Το Starvation θεωρείται λιγότερο κρίσιμο από το Deadlock, επειδή δεν είναι μοιραίο — με τη μείωση του φορτίου, το νήμα που πεινά θα εκτελεστεί τελικά. Ωστόσο, υπό συνθήκες πραγματικής χρήσης εφαρμογών Android, όπου η μνήμη και η CPU είναι περιορισμένες, το Starvation μπορεί να διαρκέσει λεπτά, δημιουργώντας μη αποδεκτή εμπειρία χρήστη.

Πώς να εντοπίσετε το Starvation

Thread Dump με επαναλαμβανόμενες λήψεις σε σύντομα διαστήματα — η βασική μέθοδος ανίχνευσης της πείνας. Εάν ένα νήμα βρίσκεται σταθερά σε κατάσταση RUNNABLE, αλλά η στοίβα κλήσεών του δεν αλλάζει σε πολλές λήψεις — αυτό είναι κλασικό σημάδι Starvation. Στο Android Studio, για αυτό χρησιμοποιείται το Android Profiler με καταγραφή της κατάστασης νημάτων στο χρόνο.

Αυτοματοποιημένη ανίχνευση είναι δυνατή μέσω παρακολούθησης του χρόνου εκτέλεσης εργασιών. Εάν μια εργασία με προβλέψιμο χρόνο εκτέλεσης (π.χ. 50 ms) εκτελείται για 5 δευτερόλεπτα ή περισσότερο — υπάρχει μεγάλη πιθανότητα Starvation. Σε εφαρμογές κινητών, το Firebase Performance Monitoring επιτρέπει τη διαμόρφωση προσαρμοσμένων ιχνών (custom traces) για κρίσιμες ενότητες και τη λήψη ειδοποιήσεων όταν ξεπερνιούνται οι τιμές κατωφλίου.

Για τη διάγνωση του Starvation λόγω μπλοκ synchronized, χρησιμοποιήστε το Java Flight Recorder (JFR) (διαθέσιμο στο Android μέσω OpenJDK API) ή το Async Profiler. Αυτά τα εργαλεία δείχνουν ποια monitors έχουν τον μεγαλύτερο χρόνο αναμονής και ποια νήματα ανταγωνίζονται για κάθε monitor. Τα δεδομένα JFR ενσωματώνονται με το IntelliJ IDEA Ultimate μέσω του ενσωματωμένου προφίλερ.

Μέθοδοι πρόληψης της πείνας νήματος

Fair Lock (ReentrantLock με σημαία true)

ReentrantLock(true) εγγυάται ότι τα νήματα λαμβάνουν το κλείδωμα με σειρά ουράς (FIFO). Σε αντίθεση με το synchronized, το fair lock δεν επιτρέπει την κατάσταση όπου το νήμα που μόλις απελευθέρωσε το κλείδωμα το καταλαμβάνει αμέσως ξανά. Αυτό εξαλείφει εντελώς το Starvation, αν και μειώνει τη συνολική απόδοση κατά 10-20% λόγω του πρόσθετου κόστους συντήρησης της ουράς.

Ατομικές δομές χωρίς κλειδώματα

Δομές δεδομένων Lock-free (ConcurrentHashMap, AtomicReference, LongAdder) εξαλείφουν το Starvation εξ ορισμού, καθώς δεν περιέχουν κλειδώματα που μπορούν να κρατηθούν από ένα νήμα. Όλες οι λειτουργίες χρησιμοποιούν εντολές CAS του επεξεργαστή, οι οποίες εγγυώνται την πρόοδο τουλάχιστον ενός νήματος σε πεπερασμένο αριθμό βημάτων. Για ανάπτυξη κινητών, προτιμήστε το ConcurrentLinkedQueue για ουρές εργασιών.

Σύντομες κρίσιμες ενότητες

Ελαχιστοποίηση του χρόνου κρατήματος του κλειδώματος — ένας καθολικός τρόπος μείωσης του κινδύνου Starvation. Βγάλτε τις βαριές λειτουργίες (δίκτυο, είσοδος/έξοδος δίσκου, σύνθετοι υπολογισμοί) εκτός του μπλοκ synchronized. Χρησιμοποιήστε ReadWriteLock για σενάρια όπου οι αναγνώστες δεν πρέπει να πεινούν λόγω σπάνιων εγγραφέων. Η βιβλιοθήκη Kotlin Coroutines παρέχει Mutex με μηχανισμό αναστολής που δεν μπλοκάρει το νήμα του λειτουργικού συστήματος.

Μεταβλητές συνθήκης και σήματα

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 παρέχει σειριακή συνέπεια — βασική ορθότητα — αλλά δεν αποτρέπει το Starvation. Για δικαιοσύνη απαιτούνται πρόσθετοι μηχανισμοί που δεν αποτελούν μέρος της προδιαγραφής JMM.

Τι είναι το Starvation στο νήμα UI του Android;

Νήμα UI (Main Thread) δεν μπορεί να πεινάσει με την κλασική έννοια, καθώς έχει την υψηλότερη προτεραιότητα. Ωστόσο, το Starvation εμφανίζεται όταν το νήμα UI περιμένει το αποτέλεσμα από ένα νήμα παρασκηνίου που πεινά. Τυπικό σενάριο: ένα AsyncTask ή μια coroutine φορτώνει δεδομένα, αλλά δεν μπορεί να αποκτήσει πρόσβαση στη βάση δεδομένων λόγω ανταγωνισμού με άλλα νήματα, και το UI παγώνει στην αναμονή.

Πώς να αποτρέψετε το Starvation σε Kotlin Coroutines;

Στις coroutines, για την πρόληψη του Starvation χρησιμοποιήστε limitedParallelism στο Dispatchers.IO για να αποφύγετε την εξάντληση νημάτων. Για συγχρονισμό, εφαρμόστε το Mutex από το kotlinx.coroutines.sync — αναστέλλει την coroutine, δεν μπλοκάρει το νήμα, μειώνοντας τον κίνδυνο πείνας. Αποφύγετε το runBlocking σε coroutines, καθώς μπορεί να καταλάβει ένα νήμα του pool και να προκαλέσει Starvation άλλων coroutines.

Σύνοψη

  • Starvation — κατάσταση όπου το νήμα είναι έτοιμο για εκτέλεση αλλά δεν λαμβάνει πόρο λόγω άδικου προγραμματισμού
  • Σε αντίθεση με το Deadlock, στην πείνα το νήμα είναι σε κατάσταση RUNNABLE και μπορεί να εκτελεστεί όταν μειωθεί το φορτίο
  • Άδικα κλειδώματα (synchronized) και λανθασμένη χρήση προτεραιοτήτων — κύριες αιτίες του Starvation
  • Fair Lock (ReentrantLock με σημαία true) εγγυάται σειρά πρόσβασης FIFO και εξαλείφει εντελώς την πείνα
  • Δομές Lock-free (ConcurrentHashMap, AtomicReference) εξαλείφουν το Starvation σε επίπεδο αρχιτεκτονικής
  • Thread Dump με επαναλαμβανόμενες λήψεις και Java Flight Recorder — αποτελεσματικές μέθοδοι διάγνωσης Starvation
  • Σύντομες κρίσιμες ενότητες και ReadWriteLock μειώνουν την πιθανότητα πείνας σε συστήματα υψηλού φορτίου

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης