Προγραμματισμός

Java 101: Κατανόηση νημάτων Java, Μέρος 3: Προγραμματισμός νημάτων και αναμονή / ειδοποίηση

Αυτό το μήνα, συνεχίζω την εισαγωγή τεσσάρων τμημάτων στα νήματα Java εστιάζοντας στον προγραμματισμό νήματος, στον μηχανισμό αναμονής / ειδοποίησης και στη διακοπή του νήματος. Θα διερευνήσετε πώς είτε ένας JVM είτε ένας προγραμματιστής νήματος λειτουργικού συστήματος επιλέγει το επόμενο νήμα για εκτέλεση. Όπως θα ανακαλύψετε, η προτεραιότητα είναι σημαντική για την επιλογή ενός προγραμματιστή νήματος. Θα εξετάσετε πώς περιμένει ένα νήμα έως ότου λάβει ειδοποίηση από άλλο νήμα προτού συνεχίσει την εκτέλεση και θα μάθετε πώς να χρησιμοποιείτε τον μηχανισμό αναμονής / ειδοποίησης για το συντονισμό της εκτέλεσης δύο νημάτων σε σχέση παραγωγού-καταναλωτή. Τέλος, θα μάθετε πώς να ξυπνάτε πρόωρα είτε έναν ύπνο είτε ένα νήμα αναμονής για τερματισμό νήματος ή άλλες εργασίες. Θα σας διδάξω επίσης πώς ένα νήμα που δεν κοιμάται ούτε περιμένει εντοπίζει ένα αίτημα διακοπής από άλλο νήμα.

Λάβετε υπόψη ότι αυτό το άρθρο (μέρος των αρχείων JavaWorld) ενημερώθηκε με νέες καταχωρίσεις κώδικα και πηγαίο κώδικα με δυνατότητα λήψης τον Μάιο του 2013.

Κατανόηση των νημάτων Java - διαβάστε ολόκληρη τη σειρά

  • Μέρος 1: Παρουσίαση νημάτων και runnables
  • Μέρος 2: Συγχρονισμός
  • Μέρος 3: Προγραμματισμός νημάτων, αναμονή / ειδοποίηση και διακοπή νημάτων
  • Μέρος 4: Ομάδες νημάτων, μεταβλητότητα, μεταβλητές τοπικού νήματος, χρονόμετρα και θάνατος νήματος

Προγραμματισμός νημάτων

Σε έναν εξιδανικευμένο κόσμο, όλα τα νήματα προγραμμάτων θα έχουν τους δικούς τους επεξεργαστές στους οποίους θα εκτελούνται. Μέχρι να έρθει η ώρα που οι υπολογιστές έχουν χιλιάδες ή εκατομμύρια επεξεργαστές, τα νήματα πρέπει συχνά να μοιράζονται έναν ή περισσότερους επεξεργαστές. Είτε η JVM είτε το λειτουργικό σύστημα της πλατφόρμας αποκρυπτογραφεί πώς να μοιράζεστε τον πόρο του επεξεργαστή μεταξύ των νημάτων - μια εργασία γνωστή ως προγραμματισμός νήματος. Αυτό το τμήμα του JVM ή του λειτουργικού συστήματος που εκτελεί προγραμματισμό νημάτων είναι ένα προγραμματιστής νημάτων.

Σημείωση: Για να απλοποιήσω τη συζήτηση προγραμματισμού νημάτων, επικεντρώνομαι στον προγραμματισμό νημάτων στο πλαίσιο ενός επεξεργαστή. Μπορείτε να κάνετε παρέκταση αυτής της συζήτησης σε πολλούς επεξεργαστές. Σας αφήνω αυτό το έργο.

Θυμηθείτε δύο σημαντικά σημεία σχετικά με τον προγραμματισμό νήματος:

  1. Η Java δεν αναγκάζει ένα VM να προγραμματίζει νήματα με συγκεκριμένο τρόπο ή να περιέχει έναν προγραμματιστή νήματος. Αυτό συνεπάγεται προγραμματισμό νημάτων που εξαρτάται από την πλατφόρμα. Επομένως, πρέπει να προσέχετε όταν γράφετε ένα πρόγραμμα Java του οποίου η συμπεριφορά εξαρτάται από τον τρόπο προγραμματισμού των νημάτων και πρέπει να λειτουργεί με συνέπεια σε διαφορετικές πλατφόρμες.
  2. Ευτυχώς, όταν γράφετε προγράμματα Java, πρέπει να σκεφτείτε πώς η Java προγραμματίζει τα νήματα μόνο όταν τουλάχιστον ένα από τα νήματα του προγράμματος σας χρησιμοποιεί πολύ τον επεξεργαστή για μεγάλες χρονικές περιόδους και τα ενδιάμεσα αποτελέσματα της εκτέλεσης αυτού του νήματος αποδεικνύονται σημαντικά. Για παράδειγμα, μια μικροεφαρμογή περιέχει ένα νήμα που δημιουργεί δυναμικά μια εικόνα. Περιοδικά, θέλετε το νήμα ζωγραφικής να σχεδιάζει τα τρέχοντα περιεχόμενα αυτής της εικόνας, ώστε ο χρήστης να μπορεί να δει πώς εξελίσσεται η εικόνα. Για να βεβαιωθείτε ότι το νήμα υπολογισμού δεν μονοπωλεί τον επεξεργαστή, σκεφτείτε τον προγραμματισμό νήματος.

Εξετάστε ένα πρόγραμμα που δημιουργεί δύο νήματα υψηλής έντασης επεξεργαστή:

Λίστα 1. SchedDemo.java

// SchedDemo.java class SchedDemo {public static void main (String [] args) {new CalcThread ("CalcThread A"). Έναρξη (); νέο CalcThread ("CalcThread B"). έναρξη (); }} Η κλάση CalcThread επεκτείνει το νήμα {CalcThread (όνομα συμβολοσειράς) {// Μετάβαση ονόματος στο επίπεδο νημάτων. σούπερ (όνομα); } διπλό calcPI () {boolean negative = true; διπλό pi = 0,0; για (int i = 3; i <100000; i + = 2) {if (αρνητικό) pi - = (1.0 / i); αλλιώς pi + = (1.0 / i); αρνητικό =! αρνητικό; } pi + = 1.0; pi * = 4.0; επιστροφή pi; } δημόσια άκυρη εκτέλεση () {για (int i = 0; i <5; i ++) System.out.println (getName () + ":" + calcPI ()); }}

ΠρόγραμμαDemo δημιουργεί δύο νήματα που το καθένα υπολογίζει την τιμή του pi (πέντε φορές) και εκτυπώνει κάθε αποτέλεσμα. Ανάλογα με τον τρόπο με τον οποίο η εφαρμογή JVM προγραμματίζει τα νήματα, ενδέχεται να δείτε την έξοδο που μοιάζει με τα ακόλουθα:

CalcThread Α: 3.1415726535897894 CalcThread Β: 3.1415726535897894 CalcThread Α: 3.1415726535897894 CalcThread Α: 3.1415726535897894 CalcThread Β: 3.1415726535897894 CalcThread Α: 3.1415726535897894 CalcThread Α: 3.1415726535897894 CalcThread Β: 3.1415726535897894 CalcThread Β: 3.1415726535897894 CalcThread Β: 3.1415726535897894

Σύμφωνα με την παραπάνω έξοδο, ο προγραμματιστής νημάτων μοιράζεται τον επεξεργαστή μεταξύ των δύο νημάτων. Ωστόσο, θα μπορούσατε να δείτε έξοδο παρόμοιο με αυτό:

CalcThread Α: 3.1415726535897894 CalcThread Α: 3.1415726535897894 CalcThread Α: 3.1415726535897894 CalcThread Α: 3.1415726535897894 CalcThread Α: 3.1415726535897894 CalcThread Β: 3.1415726535897894 CalcThread Β: 3.1415726535897894 CalcThread Β: 3.1415726535897894 CalcThread Β: 3.1415726535897894 CalcThread Β: 3.1415726535897894

Η παραπάνω έξοδος δείχνει τον προγραμματιστή νημάτων που ευνοεί το ένα νήμα έναντι του άλλου. Οι δύο παραπάνω έξοδοι απεικονίζουν δύο γενικές κατηγορίες προγραμματιστών νημάτων: πράσινο και εγγενές. Θα διερευνήσω τις διαφορές συμπεριφοράς τους σε προσεχείς ενότητες. Κατά τη συζήτηση κάθε κατηγορίας, αναφέρομαι καταστάσεις νημάτων, εκ των οποίων υπάρχουν τέσσερα:

  1. Αρχική κατάσταση: Ένα πρόγραμμα έχει δημιουργήσει ένα αντικείμενο νήματος, αλλά το νήμα δεν υπάρχει ακόμη επειδή το αντικείμενο του νήματος είναι αρχή() η μέθοδος δεν έχει κληθεί ακόμη.
  2. Κατάσταση με δυνατότητα εκτέλεσης: Αυτή είναι η προεπιλεγμένη κατάσταση του νήματος. Μετά την κλήση προς αρχή() ολοκληρώνεται, ένα νήμα μπορεί να εκτελεστεί ανεξάρτητα από το εάν εκτελείται ή όχι το νήμα, δηλαδή χρησιμοποιώντας τον επεξεργαστή. Παρόλο που πολλά νήματα μπορεί να εκτελεστούν, τρέχει μόνο ένα. Οι προγραμματιστές νημάτων καθορίζουν ποιο νήμα με δυνατότητα εκτέλεσης θα εκχωρηθεί στον επεξεργαστή.
  3. Αποκλεισμένη κατάσταση: Όταν ένα νήμα εκτελεί το ύπνος(), Περίμενε(), ή Συμμετοχή() μεθόδους, όταν ένα νήμα επιχειρεί να διαβάσει δεδομένα που δεν είναι ακόμη διαθέσιμα από ένα δίκτυο και όταν ένα νήμα περιμένει να αποκτήσει ένα κλείδωμα, αυτό το νήμα είναι σε κατάσταση αποκλεισμού: δεν τρέχει ούτε είναι σε θέση να τρέξει. (Πιθανότατα μπορείτε να σκεφτείτε άλλες στιγμές που ένα νήμα θα περιμένει κάτι να συμβεί.) Όταν ξεκλειδώσει ένα μπλοκαρισμένο νήμα, αυτό το νήμα μετακινείται στην κατάσταση με δυνατότητα εκτέλεσης.
  4. Κατάσταση τερματισμού: Μόλις η εκτέλεση αφήνει ένα νήμα τρέξιμο() μέθοδος, αυτό το νήμα είναι στην κατάσταση τερματισμού. Με άλλα λόγια, το νήμα παύει να υπάρχει.

Πώς επιλέγει ο προγραμματιστής νημάτων ποιο τρέξιμο νήμα για εκτέλεση; Αρχίζω να απαντώ σε αυτήν την ερώτηση ενώ συζητάω τον προγραμματισμό των πράσινων νημάτων. Τελειώνω την απάντηση ενώ συζητάω τον προγραμματισμό των νήσων.

Προγραμματισμός πράσινου νήματος

Δεν υποστηρίζουν όλα τα λειτουργικά συστήματα, το αρχαίο σύστημα διαμόρφωσης των Microsoft Windows 3.1, για παράδειγμα. Για τέτοια συστήματα, η Sun Microsystems μπορεί να σχεδιάσει ένα JVM που χωρίζει το μοναδικό νήμα εκτέλεσης σε πολλά νήματα. Το JVM (όχι το λειτουργικό σύστημα της υποκείμενης πλατφόρμας) παρέχει τη λογική σπειρώματος και περιέχει τον προγραμματιστή νήματος. Τα νήματα JVM είναι πράσινα νήματα, ή νήματα χρήστη.

Ο προγραμματιστής νημάτων ενός JVM προγραμματίζει πράσινα νήματα σύμφωνα με προτεραιότητα- μια σχετική σημασία του νήματος, την οποία εκφράζετε ως ακέραιος από ένα καλά καθορισμένο εύρος τιμών. Συνήθως, ένας προγραμματιστής νήματος ενός JVM επιλέγει το νήμα υψηλότερης προτεραιότητας και επιτρέπει στο νήμα να τρέξει μέχρι να τερματιστεί ή να μπλοκαριστεί. Εκείνη τη στιγμή, ο προγραμματιστής νημάτων επιλέγει ένα νήμα της επόμενης υψηλότερης προτεραιότητας. Αυτό το νήμα (συνήθως) τρέχει μέχρι να τερματιστεί ή να μπλοκαριστεί. Εάν, ενώ εκτελείται ένα νήμα, ξεκλειδώνει ένα νήμα υψηλότερης προτεραιότητας (ίσως έληξε ο χρόνος ύπνου του νήματος υψηλότερης προτεραιότητας), ο προγραμματιστής νήματος προληπτικά, ή διακόπτει, το νήμα χαμηλότερης προτεραιότητας και εκχωρεί το μη αποκλεισμένο νήμα υψηλότερης προτεραιότητας στον επεξεργαστή.

Σημείωση: Ένα τρέξιμο νήμα με την υψηλότερη προτεραιότητα δεν θα εκτελείται πάντα. Εδώ είναι το Προδιαγραφή γλώσσας Java »λαμβάνει προτεραιότητα:

Κάθε νήμα έχει ένα προτεραιότητα. Όταν υπάρχει ανταγωνισμός για την επεξεργασία πόρων, τα νήματα με υψηλότερη προτεραιότητα εκτελούνται γενικά κατά προτίμηση σε νήματα με χαμηλότερη προτεραιότητα. Αυτή η προτίμηση δεν αποτελεί, ωστόσο, εγγύηση ότι το νήμα με την υψηλότερη προτεραιότητα θα λειτουργεί πάντα και οι προτεραιότητες του νήματος δεν μπορούν να χρησιμοποιηθούν για την αξιόπιστη εφαρμογή του αμοιβαίου αποκλεισμού.

Αυτή η παραδοχή λέει πολλά για την εφαρμογή των πράσινων νήματος JVM. Αυτά τα JVM δεν έχουν την οικονομική δυνατότητα να αφήσουν τα νήματα να μπλοκάρουν επειδή αυτό θα ισοδυναμούσε με το μοναδικό νήμα εκτέλεσης του JVM. Επομένως, όταν ένα νήμα πρέπει να μπλοκάρει, όπως όταν αυτό το νήμα διαβάζει δεδομένα αργά για να φτάσει από ένα αρχείο, το JVM ενδέχεται να σταματήσει την εκτέλεση του νήματος και να χρησιμοποιήσει έναν μηχανισμό ψηφοφορίας για να προσδιορίσει πότε θα φτάσουν τα δεδομένα. Ενώ το νήμα παραμένει σταματημένο, ο προγραμματιστής νημάτων του JVM ενδέχεται να προγραμματίσει την εκτέλεση νήματος χαμηλότερης προτεραιότητας. Ας υποθέσουμε ότι τα δεδομένα φτάνουν ενώ εκτελείται το νήμα χαμηλότερης προτεραιότητας. Παρόλο που το νήμα υψηλότερης προτεραιότητας πρέπει να εκτελεστεί μόλις φτάσουν τα δεδομένα, αυτό δεν συμβαίνει έως ότου η JVM στη συνέχεια ψηφίσει το λειτουργικό σύστημα και ανακαλύψει την άφιξη. Ως εκ τούτου, το νήμα χαμηλότερης προτεραιότητας τρέχει ακόμα κι αν το νήμα υψηλότερης προτεραιότητας πρέπει να τρέξει. Πρέπει να ανησυχείτε για αυτήν την κατάσταση μόνο όταν χρειάζεστε συμπεριφορά σε πραγματικό χρόνο από την Java. Αλλά τότε η Java δεν είναι λειτουργικό σύστημα σε πραγματικό χρόνο, οπότε γιατί να ανησυχείτε;

Για να καταλάβετε ποιο τρέξιμο πράσινο νήμα γίνεται το τρέχον πράσινο νήμα, εξετάστε τα ακόλουθα. Ας υποθέσουμε ότι η εφαρμογή σας αποτελείται από τρία νήματα: το κύριο νήμα που τρέχει το κύριος() μέθοδος, ένα νήμα υπολογισμού και ένα νήμα που διαβάζει την είσοδο πληκτρολογίου. Όταν δεν υπάρχει είσοδος πληκτρολογίου, το νήμα ανάγνωσης αποκλείει. Ας υποθέσουμε ότι το νήμα ανάγνωσης έχει την υψηλότερη προτεραιότητα και το νήμα υπολογισμού έχει τη χαμηλότερη προτεραιότητα. (Για λόγους απλότητας, υποθέστε επίσης ότι δεν υπάρχουν άλλα εσωτερικά νήματα JVM.) Το Σχήμα 1 απεικονίζει την εκτέλεση αυτών των τριών νημάτων.

Τη στιγμή T0, το κύριο νήμα αρχίζει να τρέχει. Τη στιγμή Τ1, το κύριο νήμα ξεκινά το νήμα υπολογισμού. Επειδή το νήμα υπολογισμού έχει χαμηλότερη προτεραιότητα από το κύριο νήμα, το νήμα υπολογισμού περιμένει τον επεξεργαστή. Τη στιγμή Τ2, το κύριο νήμα ξεκινά το νήμα ανάγνωσης. Επειδή το νήμα ανάγνωσης έχει μεγαλύτερη προτεραιότητα από το κύριο νήμα, το κύριο νήμα περιμένει τον επεξεργαστή ενώ το νήμα ανάγνωσης εκτελείται. Τη στιγμή Τ3, το νήμα ανάγνωσης μπλοκάρει και το κύριο νήμα τρέχει. Τη στιγμή Τ4, το νήμα ανάγνωσης ξεμπλοκάρεται και εκτελείται. το κύριο νήμα περιμένει. Τέλος, τη στιγμή Τ5, το νήμα ανάγνωσης μπλοκάρει και το κύριο νήμα τρέχει. Αυτή η εναλλαγή στην εκτέλεση μεταξύ της ανάγνωσης και των κύριων νημάτων συνεχίζεται όσο εκτελείται το πρόγραμμα. Το νήμα υπολογισμού δεν τρέχει ποτέ επειδή έχει τη χαμηλότερη προτεραιότητα και έτσι λιμοκτονεί για την προσοχή του επεξεργαστή, μια κατάσταση γνωστή ως λιμού επεξεργαστή.

Μπορούμε να αλλάξουμε αυτό το σενάριο δίνοντας στο νήμα υπολογισμού την ίδια προτεραιότητα με το κύριο νήμα. Το Σχήμα 2 δείχνει το αποτέλεσμα, ξεκινώντας με το χρόνο Τ2. (Πριν από το Τ2, το Σχήμα 2 είναι πανομοιότυπο με το Σχήμα 1.)

Τη στιγμή Τ2, το νήμα ανάγνωσης τρέχει ενώ τα κύρια και τα νήματα υπολογισμού περιμένουν τον επεξεργαστή. Τη στιγμή Τ3, το νήμα ανάγνωσης μπλοκάρει και το νήμα υπολογισμού εκτελείται, επειδή το κύριο νήμα έτρεξε λίγο πριν το νήμα ανάγνωσης. Τη στιγμή Τ4, το νήμα ανάγνωσης ξεμπλοκάρεται και εκτελείται. τα κύρια νήματα και υπολογισμούς περιμένουν. Τη στιγμή Τ5, το νήμα ανάγνωσης μπλοκάρει και το κύριο νήμα τρέχει, επειδή το νήμα υπολογισμού έτρεξε λίγο πριν το νήμα ανάγνωσης. Αυτή η εναλλαγή στην εκτέλεση μεταξύ των κύριων και υπολογιστικών νημάτων συνεχίζεται όσο εκτελείται το πρόγραμμα και εξαρτάται από την εκτέλεση και το μπλοκάρισμα του νήματος υψηλότερης προτεραιότητας.

Πρέπει να εξετάσουμε ένα τελευταίο στοιχείο στον προγραμματισμό των πράσινων νημάτων. Τι συμβαίνει όταν ένα νήμα χαμηλότερης προτεραιότητας κρατά ένα κλείδωμα που απαιτεί ένα νήμα υψηλότερης προτεραιότητας; Το νήμα υψηλότερης προτεραιότητας μπλοκάρει επειδή δεν μπορεί να πάρει το κλείδωμα, πράγμα που σημαίνει ότι το νήμα υψηλότερης προτεραιότητας έχει ουσιαστικά την ίδια προτεραιότητα με το νήμα χαμηλότερης προτεραιότητας. Για παράδειγμα, ένα νήμα προτεραιότητας 6 επιχειρεί να αποκτήσει ένα κλείδωμα που κρατά ένα νήμα προτεραιότητας 3. Επειδή το νήμα προτεραιότητας 6 πρέπει να περιμένει μέχρι να αποκτήσει το κλείδωμα, το νήμα προτεραιότητας 6 καταλήγει σε μια προτεραιότητα 3 - ένα φαινόμενο γνωστό ως αντιστροφή προτεραιότητας.

Η αντιστροφή προτεραιότητας μπορεί να καθυστερήσει σημαντικά την εκτέλεση ενός νήματος υψηλότερης προτεραιότητας. Για παράδειγμα, ας υποθέσουμε ότι έχετε τρία νήματα με προτεραιότητες 3, 4 και 9. Το νήμα προτεραιότητας 3 εκτελείται και τα άλλα νήματα είναι αποκλεισμένα. Ας υποθέσουμε ότι το νήμα προτεραιότητας 3 αρπάζει ένα κλείδωμα και ξεκλειδώνει το νήμα προτεραιότητας 4. Το νήμα προτεραιότητας 4 γίνεται το τρέχον νήμα. Επειδή το νήμα προτεραιότητας 9 απαιτεί το κλείδωμα, συνεχίζει να περιμένει έως ότου το νήμα προτεραιότητας 3 απελευθερώσει το κλείδωμα. Ωστόσο, το νήμα προτεραιότητας 3 δεν μπορεί να απελευθερώσει το κλείδωμα έως ότου το νήμα προτεραιότητας 4 μπλοκάρει ή τερματιστεί. Ως αποτέλεσμα, το νήμα προτεραιότητας 9 καθυστερεί την εκτέλεση του.