Η Java ξεκινά ένα νέο νήμα. Προγραμματισμός πολλαπλών νημάτων

Ο πολυνηματικός προγραμματισμός σάς επιτρέπει να διαιρέσετε την παρουσίαση και την επεξεργασία πληροφοριών σε πολλές «ελαφριές» διαδικασίες που έχουν κοινή πρόσβαση τόσο στις μεθόδους των διαφόρων αντικειμένων εφαρμογής όσο και στα πεδία τους. Το Multithreading είναι απαραίτητο σε περιπτώσεις όπου η γραφική διεπαφή πρέπει να ανταποκρίνεται στις ενέργειες του χρήστη κατά την εκτέλεση ορισμένων επεξεργασιών πληροφοριών. Τα νήματα μπορούν να επικοινωνούν μεταξύ τους μέσω του κύριου «γονικού» νήματος από το οποίο ξεκίνησαν.

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

Οι δημιουργοί της Java παρείχαν δύο δυνατότητες για τη δημιουργία νημάτων: την υλοποίηση μιας διεπαφής Δυνατότητα λειτουργίαςκαι επέκταση της τάξης Νήμα. Η επέκταση μιας κλάσης είναι ένας τρόπος για να κληρονομηθούν μέθοδοι και μεταβλητές από μια γονική κλάση. Σε αυτήν την περίπτωση, μπορείτε να κληρονομήσετε μόνο από μία γονική κλάση Νήμα. Αυτός ο περιορισμός στην Java μπορεί να ξεπεραστεί με την υλοποίηση της διεπαφής Δυνατότητα λειτουργίας, που είναι ο πιο συνηθισμένος τρόπος δημιουργίας νημάτων.

Πλεονεκτήματα των νημάτων έναντι των διεργασιών

  • Τα νήματα είναι πολύ ελαφρύτερα από τις διεργασίες επειδή απαιτούν λιγότερο χρόνο και πόρους.
  • Η εναλλαγή περιβάλλοντος μεταξύ νημάτων είναι πολύ πιο γρήγορη από ό,τι μεταξύ διεργασιών.
  • Είναι πολύ πιο εύκολο να επιτευχθεί επικοινωνία μεταξύ νημάτων παρά μεταξύ διεργασιών.

Κύριο νήμα

Κάθε εφαρμογή java έχει τουλάχιστον ένα νήμα που τρέχει. Το νήμα από το οποίο ξεκινά η εκτέλεση του προγράμματος ονομάζεται κύριο νήμα. Μετά τη δημιουργία μιας διεργασίας, το JVM ξεκινά τυπικά να εκτελεί το κύριο νήμα με τη μέθοδο main(). Στη συνέχεια, μπορούν να ξεκινήσουν πρόσθετα νήματα όπως απαιτείται. Multithreading- δύο ή περισσότερα νήματα που εκτελούνται ταυτόχρονα σε ένα πρόγραμμα. Ένας υπολογιστής με επεξεργαστή μονού πυρήνα μπορεί να τρέξει μόνο ένα νήμα, διαιρώντας τον χρόνο της CPU μεταξύ διαφορετικών διεργασιών και νημάτων.

Κατηγορία νημάτων

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

Κατασκευαστές της τάξης Thread

Νήμα(); Νήμα (Στόχος με δυνατότητα εκτέλεσης); Thread (Στόχος με δυνατότητα εκτέλεσης, όνομα συμβολοσειράς); Thread (Όνομα συμβολοσειράς); Thread(ομάδα ThreadGroup, Runnable target); Thread(ομάδα ThreadGroup, Runnable target, String name); Thread(ομάδα ThreadGroup, όνομα συμβολοσειράς);

  • στόχος – μια παρουσία μιας κλάσης που υλοποιεί τη διεπαφή Runnable.
  • όνομα – όνομα του δημιουργημένου νήματος.
  • ομάδα – η ομάδα στην οποία ανήκει το νήμα.

Ένα παράδειγμα δημιουργίας νήματος που είναι μέρος μιας ομάδας, υλοποιεί τη διεπαφή Runnable και έχει το δικό του μοναδικό όνομα:

Runnable r = new MyClassRunnable(); ThreadGroup tg = new ThreadGroup(); Thread t = new Thread(tg, r, "myThread");

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

Αν και το κύριο νήμα δημιουργείται αυτόματα, μπορεί να ελεγχθεί. Για να γίνει αυτό, πρέπει να δημιουργήσετε ένα αντικείμενο της κλάσης Νήμακαλώντας τη μέθοδο τρέχονThread().

Μέθοδοι κλάσης νημάτων

Οι πιο συχνά χρησιμοποιούμενες μέθοδοι τάξης Νήμαγια τον έλεγχο των νημάτων:

  • long getId() - λήψη του αναγνωριστικού νήματος.
  • Συμβολοσειρά getName() - λήψη του ονόματος του νήματος.
  • int getPriority() - λήψη της προτεραιότητας του νήματος.
  • Κατάσταση getState() - προσδιορισμός της κατάστασης του νήματος.
  • void interrupt() - διακοπή της εκτέλεσης νήματος.
  • boolean isAlive() - ελέγχει εάν το νήμα εκτελείται.
  • boolean isDaemon() - ελέγχει εάν το νήμα είναι "daemon"?
  • void join() - περιμένετε να ολοκληρωθεί το νήμα.
  • void join(millis) - περιμένετε χιλιοστά του δευτερολέπτου για να ολοκληρωθεί το νήμα.
  • void notify() - "ξυπνά" ένα ξεχωριστό νήμα που περιμένει ένα "σήμα".
  • void notifyAll() - "ξυπνά" όλα τα νήματα που περιμένουν ένα "σήμα"?
  • void run() - ξεκινήστε ένα νήμα εάν το νήμα δημιουργήθηκε χρησιμοποιώντας τη διεπαφή Runnable.
  • void setDaemon(bool) - ορισμός του νήματος "daemon".
  • void setPriority(int) - προσδιορισμός της προτεραιότητας του νήματος.
  • void sleep(int) - αναστολή του νήματος για καθορισμένο χρόνο.
  • void start() - ξεκινήστε το νήμα.
  • void wait() - αναστείλει ένα νήμα έως ότου ένα άλλο νήμα καλέσει τη μέθοδο notify().
  • void wait(millis) - αναστέλλει το νήμα για χιλιοστά του δευτερολέπτου ή έως ότου ένα άλλο νήμα καλέσει τη μέθοδο notify().

Ο κύκλος ζωής ενός νήματος

Όταν εκτελείται ένα πρόγραμμα, ένα αντικείμενο Thread μπορεί να βρίσκεται σε μία από τις τέσσερις βασικές καταστάσεις: νέο, υγιές, ανθυγιεινό και παθητικό. Όταν δημιουργείται ένα νήμα, παίρνει την κατάσταση "νέο" (NEW) και δεν εκτελείται. Για να μεταφέρετε ένα νήμα από την κατάσταση "new" στην κατάσταση "running" (RUNNABLE), πρέπει να εκτελέσετε τη μέθοδο start(), η οποία καλεί τη μέθοδο run().

Ένα νήμα μπορεί να βρίσκεται σε μία από τις καταστάσεις που αντιστοιχούν στα στοιχεία του στατικά ένθετου νήματος. Αριθμός καταστάσεων:

ΝΕΟ - το νήμα έχει δημιουργηθεί αλλά δεν έχει ξεκινήσει ακόμα.
RUNNABLE - το νήμα εκτελείται.
BLOCKED - το νήμα είναι μπλοκαρισμένο.
ΑΝΑΜΟΝΗ - το νήμα περιμένει ένα άλλο νήμα να τελειώσει.
TIMED_WAITING - ένα νήμα περιμένει για κάποιο χρονικό διάστημα για να τελειώσει ένα άλλο νήμα.
ΤΕΡΜΑΤΙΣΘΗΚΕ - το νήμα έληξε.

Παράδειγμα χρήσης Thread

Το παράδειγμα ChickenEgg εξετάζει την παράλληλη λειτουργία δύο κλωστών (το κύριο νήμα και το νήμα του αυγού), στο οποίο υπάρχει μια συζήτηση σχετικά με το "ποιο ήρθε πρώτο, το αυγό ή το κοτόπουλο;" Κάθε νήμα εκφράζει τη γνώμη του μετά από μια μικρή καθυστέρηση που δημιουργείται από τη μέθοδο ChickenEgg.getTimeSleep(). Νικητής είναι το ρέμα που λέει το λόγο του τελευταίο.

Παράδειγμα πακέτου. εισαγωγή java.util.Random; κλάση Egg επεκτείνει το νήμα ( @Override public void run() ( for(int i = 0; i< 5; i++) { try { // Приостанавливаем поток sleep(ChickenEgg.getTimeSleep()); System.out.println("Яйцо"); }catch(InterruptedException e){} } } } public class ChickenEgg { public static int getTimeSleep() { final Τυχαίο τυχαίο= new Random(); int tm = random.nextInt(1000); εάν (tm< 10) tm *= 100; else if (tm < 100) tm *= 10; return tm; } public static void main(String args) { Egg egg = new Egg (); // Создание потока System.out.println("Начинаем спор: кто появился первым?"); egg.start(); // Запуск потока for(int i = 0; i < 5; i++) { try { // Приостанавливаем поток Thread.sleep(ChickenEgg.getTimeSleep()); System.out.println("Курица"); }catch(InterruptedException e){} } if(egg.isAlive()) { // Cказало ли яйцо последнее слово? try { // Ждем, пока яйцо закончит высказываться egg.join(); } catch (InterruptedException e){} System.out.println("Первым появилось яйцо!!!"); } else { //если оппонент уже закончил высказываться System.out.println("Первой появилась курица!!!"); } System.out.println("Спор закончен"); } }

Ας ξεκινήσουμε μια συζήτηση: ποιος εμφανίστηκε πρώτος; Κοτόπουλο Αυγό Κοτόπουλο Αυγό Κοτόπουλο Αυγό Κοτόπουλο Αυγό Το αυγό ήρθε πρώτο!!! Η διαμάχη τελείωσε

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

Διασύνδεση με δυνατότητα εκτέλεσης

Διεπαφή Δυνατότητα λειτουργίαςπεριέχει μόνο μία μέθοδο τρέξιμο() :

Διεπαφή με δυνατότητα εκτέλεσης ( void run();

Μέθοδος τρέξιμο()εκτελείται όταν ξεκινά το νήμα. Μετά τον καθορισμό του αντικειμένου Δυνατότητα λειτουργίαςμεταβιβάζεται σε έναν από τους κατασκευαστές κλάσεων Νήμα.

Ένα παράδειγμα κλάσης RunnableExample που υλοποιεί τη διεπαφή Runnable

Παράδειγμα πακέτου. class MyThread υλοποιεί Runnable ( Thread thread; MyThread() ( thread = new Thread(this, "Additional thread"); System.out.println("Additional thread δημιουργήθηκε " + thread); thread.start(); ) @Override public void run() ( try ( for (int i = 5; i > 0; i--) ( System.out.println("\tadditional thread: " + i); Thread.sleep(500); ) ) catch ( InterruptedException ε) ( System.out.println("\toptional νήμα διακόπηκε"); ) System.out.println("\toptional νήμα τερματίστηκε") ) δημόσια κλάση RunnableExample ( δημόσιο static void main(String args) ( new MyThread () ; δοκιμάστε ( για (int i = 5; i > 0; i--) ( System.out.println ("Κύριο νήμα: " + i); Thread.sleep(1000); ) ) catch (InterruptedException e) ( System. .out.println("Κύριο νήμα διακόπηκε" ) System.out.println("Τερματίστηκε το κύριο νήμα" ) )

Κατά την εκτέλεση του προγράμματος, εμφανίστηκε το ακόλουθο μήνυμα στην κονσόλα.

Πρόσθετο νήμα δημιουργήθηκε Νήμα[Additional thread,5,main] Κύριο νήμα: 5 επιπλέον νήμα: 5 επιπλέον νήμα: 4 Κύριο νήμα: 4 επιπλέον νήμα: 3 επιπλέον νήμα: 2 Κύριο νήμα: 3 επιπλέον νήμα: 1 επιπλέον νήμα 2 Κύριο νήμα: 1 Κύριο νήμα ολοκληρώθηκε

Συγχρονισμός νημάτων, συγχρονισμένος

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

Παράδειγμα πακέτου. κλάση CommonObject ( int counter = 0; ) class CounterThread υλοποιεί Runnable ( CommonObject res; CounterThread(CommonObject res) ( this.res = res; ) @Override public void run() ( // synchronized(res) ( res.counter = 1 ; για (int i = 1; i< 5; i++){ System.out.printf(""%s" - %d\n", Thread.currentThread().getName(), res.counter); res.counter++; try { Thread.sleep(100); } catch(InterruptedException e){} } // } } } public class SynchronizedThread { public static void main(String args) { CommonObject commonObject= new CommonObject(); for (int i = 1; i < 6; i++) { Thread t; t = new Thread(new CounterThread(commonObject)); t.setName("Поток " + i); t.start(); } } }

Το παράδειγμα ορίζει έναν κοινόχρηστο πόρο ως μια κλάση CommonObject που έχει ένα πεδίο μετρητή ακέραιου αριθμού. Αυτός ο πόρος χρησιμοποιείται εσωτερική τάξη, το οποίο δημιουργεί ένα CounterThread για να αυξήσει την τιμή του μετρητή κατά ένα σε έναν βρόχο. Όταν ξεκινά το νήμα, στο πεδίο μετρητή εκχωρείται η τιμή 1. Αφού τελειώσει το νήμα, η τιμή του μετρητή res. πρέπει να είναι ίση με 4.

Δύο γραμμές κώδικα για την κλάση CounterThread σχολιάζονται. Θα συζητηθούν παρακάτω.

Η κύρια τάξη του προγράμματος, SynchronizedThread.main, εκτελεί πέντε νήματα. Δηλαδή, κάθε νήμα πρέπει να αυξάνει την τιμή του res.counter από ένα σε τέσσερα σε έναν βρόχο. και ούτω καθεξής πέντε φορές. Αλλά το αποτέλεσμα του προγράμματος που εμφανίζεται στην κονσόλα θα είναι διαφορετικό:

"Ροή 4" - 1 "Ροή 2" - 1 "Ροή 1" - 1 "Ροή 5" - 1 "Ροή 3" - 1 "Ροή 2" - 6 "Ροή 4" - 7 "Ροή 3" - 8 "Ροή 3" 5" - 9 "Ροή 1" - 10 "Ροή 2" - 11 "Ροή 4" - 12 "Ροή 5" - 13 "Ροή 3" - 13 "Ροή 1" - 15 "Ροή 4" - 16 "Ροή 2" - 16 "Ροή 3" - 18 "Ροή 5" - 18 "Ροή 1" - 20

Δηλαδή, όλα τα νήματα λειτουργούν με τον κοινόχρηστο πόρο res.counter ταυτόχρονα, εναλλάσσοντας την τιμή.

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

Κλείδωμα σε επίπεδο αντικειμένου

Μπορείτε να κλειδώσετε έναν κοινόχρηστο πόρο σε επίπεδο αντικειμένου, αλλά δεν μπορείτε να χρησιμοποιήσετε πρωτόγονους τύπους για αυτό το σκοπό. Στο παράδειγμα, τα σχόλια γραμμής στην κλάση CounterThread θα πρέπει να αφαιρεθούν, μετά την οποία ο κοινόχρηστος πόρος θα κλειδωθεί μόλις τον αποκτήσει ένα από τα νήματα. τα υπόλοιπα νήματα θα περιμένουν στην ουρά για να κυκλοφορήσει ο πόρος. Το αποτέλεσμα του προγράμματος κατά το συγχρονισμό της πρόσβασης σε έναν κοινόχρηστο πόρο θα αλλάξει δραματικά:

"Ροή 1" - 1 "Ροή 1" - 2 "Ροή 1" - 3 "Ροή 1" - 4 "Ροή 5" - 1 "Ροή 5" - 2 "Ροή 5" - 3 "Ροή 5" - 4 "Ροή 5" - 4 "Ροή 5" 4" - 1 "Ροή 4" - 2 "Ροή 4" - 3 "Ροή 4" - 4 "Ροή 3" - 1 "Ροή 3" - 2 "Ροή 3" - 3 "Ροή 3" - 4 "Ροή 2" - 1 "Ροή 2" - 2 "Ροή 2" - 3 "Ροή 2" - 4

Ο παρακάτω κώδικας δείχνει τον τρόπο χρήσης του χειριστή συγχρονισμένηγια να αποκλείσετε την πρόσβαση σε ένα αντικείμενο.

Συγχρονισμένο (αντικείμενο) ( // άλλος ασφαλής κωδικός νήματος )

Κλείδωμα μεθόδου και επιπέδου τάξης

Μπορείτε να αποκλείσετε την πρόσβαση σε πόρους σε επίπεδο μεθόδου και κλάσης. Ο παρακάτω κώδικας δείχνει ότι εάν υπάρχουν πολλές παρουσίες της κλάσης DemoClass κατά την εκτέλεση του προγράμματος, τότε μόνο ένα νήμα μπορεί να εκτελέσει τη μέθοδο demoMethod() θα αποκλειστεί η πρόσβαση στη μέθοδο. Αυτό είναι απαραίτητο όταν πρέπει να καταστήσετε ασφαλή το νήμα ορισμένων πόρων.

Δημόσια κλάση DemoClass ( δημόσια συγχρονισμένη στατική κενή demoMethod() ( // ... ) ) // ή δημόσια κλάση DemoClass (δημόσια κενό demoMethod() ( συγχρονισμένη (DemoClass.class) ( // ... ) )

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

Μερικές σημαντικές σημειώσεις σχετικά με τη χρήση του συγχρονισμένου

  1. Ο συγχρονισμός σε Java διασφαλίζει ότι δύο νήματα δεν μπορούν να εκτελέσουν μια συγχρονισμένη μέθοδο ταυτόχρονα.
  2. Χειριστής συγχρονισμένημπορεί να χρησιμοποιηθεί μόνο με μεθόδους και μπλοκ κώδικα, τα οποία μπορεί να είναι ή να μην είναι στατικά.
  3. Εάν ένα από τα νήματα ξεκινήσει να εκτελεί μια συγχρονισμένη μέθοδο ή μπλοκ, τότε αυτή η μέθοδος/μπλοκ αποκλείεται. Όταν ένα νήμα εξέρχεται από μια συγχρονισμένη μέθοδο ή μπλοκ, το JVM απελευθερώνει το κλείδωμα. Το κλείδωμα απελευθερώνεται ακόμα και αν το νήμα εγκαταλείψει τη συγχρονισμένη μέθοδο μετά την ολοκλήρωση λόγω τυχόν σφαλμάτων ή εξαιρέσεων.
  4. Ο συγχρονισμός σε Java δημιουργεί ένα NullPointerException εάν το αντικείμενο που χρησιμοποιείται στο συγχρονισμένο μπλοκ δεν είναι καθορισμένο, π.χ. ισούται με μηδενικό.
  5. Οι συγχρονισμένες μέθοδοι σε Java εισάγουν πρόσθετο κόστος στην απόδοση της εφαρμογής. Επομένως, θα πρέπει να χρησιμοποιείτε το συγχρονισμό όταν είναι απολύτως απαραίτητο.
  6. Σύμφωνα με τις προδιαγραφές γλώσσας, δεν μπορείτε να χρησιμοποιήσετε συγχρονισμένηστον κατασκευαστή, γιατί θα οδηγήσει σε σφάλμα μεταγλώττισης.

Σημείωση:Για να συγχρονίσετε νήματα, μπορείτε να χρησιμοποιήσετε τα αντικείμενα συγχρονισμού του πακέτου του Synchroniser java.util.concurrent.

Αμοιβαίο μπλοκάρισμα

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

Οι κύριες προϋποθέσεις για να προκύψουν αδιέξοδα σε μια εφαρμογή πολλαπλών νημάτων είναι:

  • την παρουσία πόρων που θα πρέπει να είναι διαθέσιμοι σε ένα μόνο νήμα ανά πάσα στιγμή·
  • κατά την απόκτηση ενός πόρου, το νήμα προσπαθεί να αποκτήσει έναν άλλο μοναδικό πόρο.
  • δεν υπάρχει μηχανισμός για την απελευθέρωση ενός πόρου όταν διατηρείται για μεγάλο χρονικό διάστημα.
  • Κατά την εκτέλεση, πολλά νήματα μπορούν να αποκτήσουν διαφορετικούς μοναδικούς πόρους και να περιμένουν το ένα το άλλο να τους απελευθερώσει.

Επικοινωνία μεταξύ νημάτων σε Java, περιμένετε και ειδοποιήστε

Όταν τα νήματα αλληλεπιδρούν, υπάρχει συχνά η ανάγκη να αναστείλετε ορισμένα νήματα και στη συνέχεια να τα ειδοποιήσετε για την ολοκλήρωση ορισμένων ενεργειών σε άλλα νήματα. Για παράδειγμα, οι ενέργειες του πρώτου νήματος εξαρτώνται από το αποτέλεσμα των ενεργειών του δεύτερου νήματος και είναι απαραίτητο να ειδοποιηθεί με κάποιο τρόπο το πρώτο νήμα ότι το δεύτερο νήμα έχει εκτελέσει/ολοκληρώσει συγκεκριμένη εργασία. Για τέτοιες περιπτώσεις, χρησιμοποιούνται οι ακόλουθες μέθοδοι:

  • Wait() - απελευθερώνει την οθόνη και θέτει το νήμα κλήσης σε κατάσταση αναμονής έως ότου ένα άλλο νήμα καλέσει τη μέθοδο notify().
  • notify() - συνεχίζει την εργασία του νήματος στο οποίο είχε κληθεί προηγουμένως η μέθοδος wait().
  • notifyAll() - συνεχίζει την εργασία όλων των νημάτων που είχαν προηγουμένως κληθεί η μέθοδος wait().

Όλες αυτές οι μέθοδοι καλούνται μόνο από συγχρονισμένο περιβάλλον (ένα συγχρονισμένο μπλοκ ή μέθοδος).

Ας εξετάσουμε το παράδειγμα «Παραγωγός-Κατάστημα-Καταναλωτής». Μέχρι ο κατασκευαστής να παραδώσει το προϊόν στην αποθήκη, ο καταναλωτής δεν μπορεί να το παραλάβει. Ας υποθέσουμε ότι ένας κατασκευαστής πρέπει να παρέχει 5 μονάδες ενός συγκεκριμένου προϊόντος. Κατά συνέπεια, ο καταναλωτής πρέπει να λάβει ολόκληρο το προϊόν. Αλλά, ταυτόχρονα, δεν μπορούν να βρίσκονται ταυτόχρονα περισσότερες από 3 μονάδες εμπορευμάτων στην αποθήκη. Κατά την εφαρμογή αυτό το παράδειγμαχρησιμοποιούμε μεθόδους Περίμενε()Και κοινοποιώ().

Καταχώρηση της κατηγορίας Store

Παράδειγμα πακέτου. public class Store ( ιδιωτικός μετρητής int = 0; δημόσιος συγχρονισμένος void get() ( while (counter< 1) { try { wait(); } catch (InterruptedException e) {} } counter--; System.out.println("-1: товар забрали"); System.out.println("\tколичество товара на складе: " + counter); notify(); } public synchronized void put() { while (counter >= 3) ( try ( wait(); )catch (InterruptedException e) () ) counter++; System.out.println("+1: προστέθηκε προϊόν"); System.out.println("\tποσότητα αγαθών σε απόθεμα: " + μετρητής); κοινοποιώ();

) ) Η κλάση Store περιέχει δύο συγχρονισμένες μεθόδους για την ανάκτηση ενός προϊόντοςπαίρνω() και να προσθέσετε ένα προϊόνβάζω()< 1, то вызывается метод Περίμενε(). Με την παραλαβή των εμπορευμάτων ελέγχεται ο πάγκος. Εάν δεν υπάρχει προϊόν σε απόθεμα, τότε υπάρχει πάγκος Η κλάση Store περιέχει δύο συγχρονισμένες μεθόδους για την ανάκτηση ενός προϊόντος, το οποίο απελευθερώνει την οθόνη αντικειμένου Store και αποκλείει την εκτέλεση της μεθόδου κοινοποιώ().

μέχρι να κληθεί η μέθοδος για αυτήν την οθόνη κοινοποιώ()Κατά την προσθήκη ενός προϊόντος ελέγχεται και η ποσότητα του προϊόντος σε απόθεμα. Εάν υπάρχουν περισσότερες από 3 μονάδες εμπορευμάτων στην αποθήκη, τότε η παράδοση των αγαθών αναστέλλεται και η μέθοδος καλείται Η κλάση Store περιέχει δύο συγχρονισμένες μεθόδους για την ανάκτηση ενός προϊόντος, το οποίο μεταφέρει τον έλεγχο στη μέθοδο

για να τερματίσετε τον βρόχο while().

Καταχωρίσεις των κατηγοριών Παραγωγού και Καταναλωτή Δυνατότητα λειτουργίαςΟι κατηγορίες Παραγωγός και Καταναλωτής υλοποιούν τη διεπαφή τρέξιμο()έχουν επαναπροσδιοριστεί. Οι κατασκευαστές αυτών των κλάσεων λαμβάνουν ως παράμετρο ένα αντικείμενο Store. Όταν αυτά τα αντικείμενα ξεκινούν ως ξεχωριστά νήματα σε έναν βρόχο, οι μέθοδοι put() και get() της κλάσης Store καλούνται να «προσθέσουν» και να «πάρουν» ένα προϊόν.

Παράδειγμα πακέτου. public class Producer υλοποιεί Runnable ( Store store; Producer(Store store) ( this.store=store; ) @Override public void run() ( for (int i = 1; i< 6; i++) { store.put(); } } } public class Consumer implements Runnable { Store store; Consumer(Store store) { this.store=store; } @Override public void run(){ for (int i = 1; i < 6; i++) { store.get(); } } }

Καταχώριση εμπορικής κατηγορίας

Στο κύριο νήμα της κλάσης Trade (στη μέθοδο κύριος) Δημιουργούνται αντικείμενα Παραγωγός-Κατάστημα-Καταναλωτής και ξεκινούν τα νήματα παραγωγού και καταναλωτή.

Παράδειγμα πακέτου. public class Trade ( public static void main(String args) ( Store store = new Store(); Producer producer = new Producer(store); Consumer καταναλωτής = νέος Consumer(store); new Thread(producer).start(); new Thread(consumer).start();

Κατά την εκτέλεση του προγράμματος, στην κονσόλα θα εμφανιστούν τα ακόλουθα μηνύματα:

1: προστιθέμενα αγαθά ποσότητα εμπορευμάτων στην αποθήκη: 1 +1: προστιθέμενα αγαθά ποσότητα εμπορευμάτων στην αποθήκη: 2 +1: προστιθέμενα αγαθά ποσότητα εμπορευμάτων στην αποθήκη: 3 -1: ληφθέντα εμπορεύματα ποσότητα εμπορευμάτων στην αποθήκη: 2 -1: εμπορεύματα που ελήφθησαν ποσότητα εμπορευμάτων στην αποθήκη: 1 -1: εμπορεύματα ελήφθησαν ποσότητα εμπορευμάτων στην αποθήκη: 0 +1: προστέθηκαν εμπορεύματα ποσότητα εμπορευμάτων στην αποθήκη: 1 +1: προστέθηκαν αγαθά ποσότητα αγαθών στην αποθήκη : 2 -1: παραλήφθηκαν εμπορεύματα ποσότητα εμπορευμάτων στην αποθήκη: 1 -1 : τα εμπορεύματα αφαιρέθηκαν ποσότητα εμπορευμάτων στην αποθήκη: 0

Δαιμονική κλωστή, δαίμονας

Μια εφαρμογή Java κλείνει όταν τελειώσει το τελευταίο νήμα της. Ακόμα κι αν η μέθοδος main() έχει ήδη ολοκληρωθεί, αλλά τα νήματα που δημιούργησε εξακολουθούν να εκτελούνται, το σύστημα θα περιμένει να ολοκληρωθούν.

Ωστόσο, αυτός ο κανόνας δεν ισχύει για νήματα daemon ( δαίμονας). Εάν το τελευταίο κανονικό νήμα μιας διαδικασίας έχει ολοκληρωθεί και μόνο δαίμοναςνήματα, θα τερματιστούν αναγκαστικά και η εφαρμογή θα τελειώσει. Πιο συχνά δαίμοναςΤα νήματα χρησιμοποιούνται για την εκτέλεση εργασιών παρασκηνίου που εξυπηρετούν τη διαδικασία κατά τη διάρκεια της ζωής της.

Η δήλωση ενός νήματος ως δαίμονα είναι αρκετά απλή. Για να το κάνετε αυτό, πρέπει να καλέσετε τη μέθοδο setDaemon(true) πριν ξεκινήσετε το νήμα. Ελέγξτε αν το νήμα είναι δαίμονας"Αυτό μπορεί να γίνει καλώντας τη μέθοδο isDaemon(). Ως παράδειγμα χρήσης μιας ροής δαίμονα, μπορείτε να εξετάσετε την κλάση Trade, η οποία θα είχε την ακόλουθη μορφή:

Παράδειγμα πακέτου. public class Trade ( public static void main(String args) ( Producer producer = new Producer(store); Consumer consumer = new Consumer(store); // new Thread(producer).start(); // new Thread(consumer) .start(); ");

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

Thread και Runnable, τι να επιλέξω;

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

Επέκταση τάξης ΝήμαΣυνιστάται να το χρησιμοποιήσετε εάν είναι απαραίτητο να παρακάμψετε άλλες μεθόδους κλάσης εκτός από τη μέθοδο run().

Προτεραιότητες εκτέλεσης και νηστεία

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

Ένα μη προφανές παράδειγμα "ασιτίας" του νήματος παρέχεται από τη μέθοδο Οριστικοποιώ(), το οποίο παρέχει τη δυνατότητα εκτέλεσης κώδικα πριν το αντικείμενο συλλεχθεί σκουπίδια. Ωστόσο, η προτεραιότητα του νήματος οριστικοποίησης είναι χαμηλή. Κατά συνέπεια, οι προϋποθέσεις για την ασιτία ροής προκύπτουν όταν οι μέθοδοι Οριστικοποιώ()Τα αντικείμενα ξοδεύουν πολύ χρόνο (υψηλές καθυστερήσεις) σε σύγκριση με τον υπόλοιπο κώδικα.

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

Λήψη παραδειγμάτων

Μπορείτε να κατεβάσετε τα παραδείγματα πολλαπλών νημάτων και συγχρονισμού νημάτων που αναφέρονται στη σελίδα με τη μορφή έργου Eclipse (14 Kb).

Οποιαδήποτε εφαρμογή μπορεί να έχει πολλαπλές διεργασίες (στιγμιότυπα). Κάθε μία από αυτή τη διαδικασία μπορεί να αντιστοιχιστεί είτε ως ένα νήμα είτε ως πολλαπλά νήματα.

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

Σε αυτό το σεμινάριο, θα μάθουμε-

Τι είναι το Single Thread;

Ένα μόνο νήμα είναι βασικά μια ελαφριά και η μικρότερη μονάδα επεξεργασίας. Η Java χρησιμοποιεί νήματα χρησιμοποιώντας μια "Κλάση Νημάτων".

Υπάρχουν δύο τύποι νημάτων - νήμα χρήστη και νήμα δαίμονας(τα νήματα daemon χρησιμοποιούνται όταν θέλουμε να καθαρίσουμε την εφαρμογή και χρησιμοποιούνται στο παρασκήνιο).

Όταν ξεκινά μια εφαρμογή για πρώτη φορά, δημιουργείται νήμα χρήστη. Αναρτήστε το, μπορούμε να δημιουργήσουμε πολλά νήματα χρηστών και νήματα δαίμονα.

Παράδειγμα μεμονωμένου νήματος:

Υποβολή πακέτου; δημόσια κλάση GuruThread ( δημόσιο static void main (String args) ( System.out.println ("Single Thread"); ) )

Πλεονεκτήματα του απλού νήματος:

  • Μειώνει την επιβάρυνση της εφαρμογής καθώς εκτελείται ένα νήμα στο σύστημα
  • Επίσης, μειώνει το κόστος συντήρησης της εφαρμογής.

Τι είναι το Multithreading;

Το Multithreading στη java είναι μια διαδικασία εκτέλεσης δύο ή περισσότερων νημάτων ταυτόχρονα για τη μέγιστη χρήση της CPU.

Οι εφαρμογές πολλαπλών νημάτων είναι όπου δύο ή περισσότερα νήματα εκτελούνται ταυτόχρονα. ως εκ τούτου είναι επίσης γνωστό ως Συγχρονισμόςστην Java. Αυτό το multitasking πραγματοποιείται όταν πολλαπλές διεργασίες μοιράζονται κοινούς πόρους όπως CPU, μνήμη κ.λπ.

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

Παράδειγμα πολλαπλών νημάτων:

Υποβολή πακέτου; δημόσια κλάση GuruThread1 υλοποιεί Runnable ( δημόσιο static void main(String args) ( Thread guruThread1 = new Thread("Guru1"); Thread guruThread2 = new Thread("Guru2"); guruThread1.start(); guruThread2.st); .out.println("Τα ονόματα νημάτων ακολουθούν:" System.out.println(guruThread1.getName());

Πλεονεκτήματα του multithread:

  • Οι χρήστες δεν είναιαποκλειστεί επειδή τα νήματα είναι ανεξάρτητα και μπορούμε να εκτελέσουμε πολλές λειτουργίες κατά καιρούς
  • Ως εκ τούτου, τα νήματα είναι ανεξάρτητα, τα άλλα νήματα δεν θα επηρεαστούν εάν ένα νήμα πληροί μια εξαίρεση.

Κύκλος ζωής νήματος σε Java

Ο κύκλος ζωής ενός νήματος:

Υπάρχουν διάφορα στάδια του κύκλου ζωής του νήματος όπως φαίνεται στο παραπάνω διάγραμμα:

  1. Δυνατότητα λειτουργίας
  2. Τρέξιμο
  3. Αναμονή
  1. Νέος:Σε αυτή τη φάση, το νήμα δημιουργείται χρησιμοποιώντας την κλάση "Thread class". Παραμένει σε αυτήν την κατάσταση μέχρι το πρόγραμμα ξεκινάτο νήμα. Είναι επίσης γνωστό ως γεννημένο νήμα.
  2. Με δυνατότητα εκτέλεσης:Σε αυτή τη σελίδα, το στιγμιότυπο του νήματος καλείται με μια μέθοδο έναρξης. Ο έλεγχος νήματος δίνεται στον προγραμματιστή για να ολοκληρώσει την εκτέλεση. Εξαρτάται από τον προγραμματιστή, αν θα τρέξει το νήμα.
  3. Τρέξιμο:Όταν το νήμα αρχίσει να εκτελείται, τότε η κατάσταση αλλάζει σε κατάσταση "εκτέλεσης". Ο χρονοπρογραμματιστής επιλέγει ένα νήμα από το σύνολο νημάτων και αρχίζει να εκτελείται στην εφαρμογή.
  4. Αναμονή:Αυτή είναι η κατάσταση όταν ένα νήμα πρέπει να περιμένει. Καθώς υπάρχουν πολλά νήματα που εκτελούνται στην εφαρμογή, υπάρχει α ανάγκη γιασυγχρονισμός μεταξύ των νημάτων. Ως εκ τούτου, ένα νήμα πρέπει να περιμένει, μέχρι να εκτελεστεί το άλλο νήμα. Επομένως, αυτή η κατάσταση αναφέρεται ως κατάσταση αναμονής.
  5. Νεκρός:Αυτή είναι η κατάσταση όταν το νήμα τερματίζεται. Το νήμα βρίσκεται σε κατάσταση λειτουργίας και μόλις ολοκληρώσει την επεξεργασία βρίσκεται σε "νεκρή κατάσταση".

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

Μέθοδος Περιγραφή
αρχή() Αυτή η μέθοδος ξεκινά την εκτέλεση του νήματος και το JVM καλεί τη μέθοδο run() στο νήμα.
Ύπνος (int χιλιοστά του δευτερολέπτου) Αυτή η μέθοδος κάνει το νήμα να κοιμάται, επομένως η εκτέλεση του νήματος θα σταματήσει για χιλιοστά του δευτερολέπτου που παρέχονται και μετά από αυτό, το νήμα αρχίζει και πάλι να εκτελείται. Αυτό βοηθά στο συγχρονισμό των νημάτων.
getName() Επιστρέφει το όνομα του νήματος.
setPriority (in new priority) Αλλάζει την προτεραιότητα του νήματος.
απόδοση παραγωγής() Το ρεύμα προκαλεί νήμα σε στάση και άλλα νήματα να εκτελεστούν.

Παράδειγμα:Σε αυτό το παράδειγμα θα δημιουργήσουμε ένα νήμα και θα εξερευνήσουμε ενσωματωμένες μεθόδους που είναι διαθέσιμες για νήματα.

Υποβολή πακέτου; δημόσια κλάση thread_example1 υλοποιεί Runnable ( @Override public void run() ( ) public static void main(String args) ( Thread guruthread1 = new Thread(); guruthread1.start(); try ( guruthread1.sleep(1000); ) catch ( InterruptedException e) ( // TODO Αυτόματη δημιουργία μπλοκ αλίευσης e.printStackTrace(); ) guruthread1.setPriority(1);

Επεξήγηση του κώδικα:

Κωδικός Γραμμή 2:Δημιουργούμε μια κλάση "thread_Example1" που υλοποιεί τη διεπαφή Runnable (θα πρέπει να υλοποιηθεί από οποιαδήποτε κλάση οι παρουσίες της οποίας προορίζονται να εκτελεστούν από το νήμα.)

Κωδικός Γραμμή 4:Αντικαθιστά τη μέθοδο εκτέλεσης της διεπαφής με δυνατότητα εκτέλεσης καθώς είναι υποχρεωτική η παράκαμψη αυτής της μεθόδου

Κωδικός Γραμμή 6:Εδώ έχουμε ορίσει την κύρια μέθοδο με την οποία θα ξεκινήσουμε την εκτέλεση του νήματος.

Κωδικός Γραμμή 7:Εδώ δημιουργούμε ένα νέο όνομα νήματος ως "guruthread1" εγκαινιάζοντας μια νέα κατηγορία νήματος.

Κωδικός Γραμμή 8:θα χρησιμοποιήσουμε τη μέθοδο "start" του νήματος χρησιμοποιώντας το παράδειγμα "guruthread1". Εδώ το νήμα θα ξεκινήσει να εκτελείται.

Κωδικός Γραμμή 10:Εδώ χρησιμοποιούμε τη μέθοδο "sleep" του νήματος χρησιμοποιώντας το παράδειγμα "guruthread1". Ως εκ τούτου, το νήμα θα κοιμάται για 1000 χιλιοστά του δευτερολέπτου.

Κωδικός 9-14:Εδώ έχουμε βάλει τη μέθοδο ύπνου στο μπλοκ try catch καθώς υπάρχει επιλεγμένη εξαίρεση που συμβαίνει δηλ. Διακοπείσα εξαίρεση.

Κωδικός Γραμμή 15:Εδώ ορίζουμε την προτεραιότητα του νήματος στο 1 από όποια προτεραιότητα ήταν

Κωδικός Γραμμή 16:Εδώ παίρνουμε την προτεραιότητα του νήματος χρησιμοποιώντας την getPriority()

Κωδικός Γραμμή 17:Εδώ εκτυπώνουμε την τιμή που ανακτήθηκε από το getPriority

Κωδικός Γραμμή 18:Εδώ γράφουμε ένα κείμενο που τρέχει το νήμα.

Το 5 είναι η προτεραιότητα του νήματος και το Thread Running είναι το κείμενο που είναι η έξοδος του κώδικά μας.

Συγχρονισμός νημάτων Java

Στο multithreading υπάρχει η ασύγχρονη συμπεριφορά των προγραμμάτων. Εάν ένα νήμα εγγράφει ορισμένα δεδομένα και ένα άλλο νήμα που διαβάζει δεδομένα ταυτόχρονα, μπορεί να δημιουργήσει ασυνέπεια στην εφαρμογή.

Όταν υπάρχει ανάγκη πρόσβασης στους κοινόχρηστους πόρους από δύο ή περισσότερα νήματα, τότε χρησιμοποιείται η προσέγγιση συγχρονισμού.

Η Java έχει παράσχει συγχρονισμένες μεθόδους για την εφαρμογή συγχρονισμένης συμπεριφοράς.

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

Με αυτόν τον τρόπο, ο συγχρονισμός βοηθά σε μια εφαρμογή πολλαπλών νημάτων. Ένα νήμα πρέπει να περιμένει έως ότου το άλλο νήμα ολοκληρώσει την εκτέλεσή του μόνο τότε επιτρέπεται να εκτελεστούν τα άλλα νήματα.

Μπορεί να γραφτεί με την ακόλουθη μορφή:

Synchronized(object) ( //Block of statements to synchronized )

Παράδειγμα Java Multithreading

Σε αυτό το παράδειγμα, θα πάρουμε δύο νήματα και θα φέρουμε τα ονόματα του νήματος.

Παράδειγμα 1:

Υποβολή πακέτου GuruThread1.java. δημόσια κλάση GuruThread1 υλοποιεί Runnable( /** * @param args */ public static void main(String args) ( Thread guruThread1 = new Thread("Guru1"); Thread guruThread2 = new Thread("Guru2"); guruThread1.start( guruThread2.start("System.out.println"; Παράκαμψη δημόσιας κενού εκτέλεσης() ( ) )

Επεξήγηση του κώδικα:

Κωδικός Γραμμή 3:Έχουμε πάρει μια κλάση "GuruThread1" που υλοποιεί το Runnable (θα πρέπει να υλοποιηθεί από οποιαδήποτε κλάση της οποίας οι παρουσίες προορίζονται να εκτελεστούν από το νήμα.)

Κωδικός Γραμμή 8:Αυτή είναι η κύρια μέθοδος της τάξης

Κωδικός Γραμμή 9:Εδώ δημιουργούμε την κλάση Thread και δημιουργούμε ένα στιγμιότυπο με το όνομα "guruThread1" και δημιουργούμε ένα νήμα.

Κωδικός Γραμμή 10:Εδώ δημιουργούμε την κλάση Thread και δημιουργούμε ένα στιγμιότυπο με το όνομα "guruThread2" και δημιουργούμε ένα νήμα.

Κωδικός Γραμμή 11:Ξεκινάμε το νήμα δηλ. guruThread1.

Κωδικός Γραμμή 12:Ξεκινάμε το νήμα δηλ. guruThread2.

Κωδικός Γραμμή 13:Εξαγωγή του κειμένου ως "Τα ονόματα των νημάτων είναι τα ακόλουθα:"

Κωδικός Γραμμή 14:Λήψη του ονόματος του νήματος 1 χρησιμοποιώντας τη μέθοδο getName() της κλάσης νήματος.

Κωδικός Γραμμή 15:Λήψη του ονόματος του νήματος 2 χρησιμοποιώντας τη μέθοδο getName() της κλάσης νήματος.

Όταν εκτελείτε τον παραπάνω κώδικα, λαμβάνετε την ακόλουθη έξοδο:

Τα ονόματα των νημάτων εμφανίζονται εδώ ως

  • Γκουρού 1
  • Γκουρού2

Παράδειγμα 2:

Σε αυτό το παράδειγμα, θα μάθουμε για τις μεθόδους παράκαμψης run() και start() μιας διεπαφής με δυνατότητα εκτέλεσης και θα δημιουργήσουμε δύο νήματα αυτής της κλάσης και θα τα εκτελέσουμε ανάλογα.

Επίσης, παρακολουθούμε δύο μαθήματα,

  • Ένα που θα υλοποιήσει τη διεπαφή με δυνατότητα εκτέλεσης και
  • Ένα άλλο που θα έχει την κύρια μέθοδο και θα εκτελείται ανάλογα.
demotest πακέτου? δημόσια κλάση GuruThread2 ( δημόσιο στατικό κενό main(Args συμβολοσειράς) ( // TODO Αυτόματα δημιουργημένη μέθοδος στέλεχος GuruThread3 threadguru1 = νέο GuruThread3("guru1"); threadguru1.start(); GuruThread3 threadguru2 = νέο Guruth"); .start(); για (int i = 0; i< 4; i++) { System.out.println(i); System.out.println(guruname); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Thread has been interrupted"); } } } public void start() { System.out.println("Thread started"); if (guruthread == null) { guruthread = new Thread(this, guruname); guruthread.start(); } } }

Επεξήγηση του κώδικα:

Κωδικός Γραμμή 2:Εδώ παίρνουμε μια κλάση "GuruThread2" που θα έχει την κύρια μέθοδο σε αυτήν.

Κωδικός Γραμμή 4:Εδώ παίρνουμε μια κύρια μέθοδο της τάξης.

Κωδικός Γραμμή 6-7:Εδώ δημιουργούμε μια παρουσία της κλάσης GuruThread3 (η οποία δημιουργείται στις παρακάτω γραμμές του κώδικα) ως "threadguru1" και ξεκινάμε το νήμα.

Κωδικός Γραμμή 8-9:Εδώ δημιουργούμε μια άλλη παρουσία της κλάσης GuruThread3 (η οποία δημιουργείται στις παρακάτω γραμμές του κώδικα) ως "threadguru2" και ξεκινάμε το νήμα.

Κωδικός Γραμμή 11:Εδώ δημιουργούμε μια κλάση "GuruThread3" που υλοποιεί τη διεπαφή με δυνατότητα εκτέλεσης (θα πρέπει να υλοποιηθεί από οποιαδήποτε κλάση της οποίας οι παρουσίες προορίζονται να εκτελεστούν από το νήμα.)

Κωδικός Γραμμή 13-14:Παίρνουμε δύο μεταβλητές κλάσης από τις οποίες η μία είναι της κλάσης τύπου thread και η άλλη της κλάσης string.

Κωδικός Γραμμή 15-18:παρακάμπτουμε τον κατασκευαστή GuruThread3, ο οποίος παίρνει ένα όρισμα ως τύπο συμβολοσειράς (που είναι το όνομα νημάτων) που εκχωρείται στη μεταβλητή κλάσης guruname και επομένως το όνομα του νήματος αποθηκεύεται.

Κωδικός Γραμμή 20:Εδώ παρακάμπτουμε τη μέθοδο run() της διεπαφής με δυνατότητα εκτέλεσης.

Κωδικός Γραμμή 21:Εξάγουμε το όνομα νήματος χρησιμοποιώντας την πρόταση println.

Κωδικός Γραμμή 22-31:Εδώ χρησιμοποιούμε έναν βρόχο for με μετρητή αρχικοποιημένο στο 0, και δεν πρέπει να είναι μικρότερος από 4 (μπορούμε να πάρουμε οποιονδήποτε αριθμό, επομένως εδώ ο βρόχος θα τρέξει 4 φορές) και αυξάνοντας τον μετρητή. Εκτυπώνουμε το όνομα του νήματος και επίσης κάνουμε το νήμα να αδράνει για 1000 χιλιοστά του δευτερολέπτου μέσα σε ένα μπλοκ try-catch καθώς η μέθοδος ύπνου αυξάνει την επιλεγμένη εξαίρεση.

Κωδικός Γραμμή 33:Εδώ παρακάμπτουμε τη μέθοδο έναρξης της διεπαφής με δυνατότητα εκτέλεσης.

Κωδικός Γραμμή 35:Βγάζουμε το κείμενο "Το νήμα ξεκίνησε".

Κωδικός Γραμμή 36-40:Εδώ παίρνουμε μια συνθήκη if για να ελέγξουμε αν η μεταβλητή κλάσης guruthread έχει αξία ή όχι. Εάν είναι μηδενικό, τότε δημιουργούμε ένα στιγμιότυπο χρησιμοποιώντας κλάση νήματος που παίρνει το όνομα ως παράμετρο (τιμή για την οποία έχει εκχωρηθεί στον κατασκευαστή). Μετά από αυτό το νήμα ξεκινά χρησιμοποιώντας τη μέθοδο start().

Όταν εκτελείτε τον παραπάνω κώδικα, λαμβάνετε την ακόλουθη έξοδο:

Υπάρχουν δύο νήματα, επομένως, λαμβάνουμε δύο φορές το μήνυμα "Το νήμα ξεκίνησε".

Παίρνουμε τα ονόματα του νήματος όπως τα έχουμε βγάλει.

Μπαίνει στον βρόχο for όπου εκτυπώνουμε το όνομα του μετρητή και του νήματος και ο μετρητής ξεκινά με 0.

Ο βρόχος εκτελείται τρεις φορές και ενδιάμεσα το νήμα είναι σε αδράνεια για 1000 χιλιοστά του δευτερολέπτου.

Ως εκ τούτου, πρώτα, παίρνουμε guru1 μετά guru2 και μετά πάλι guru2 επειδή το νήμα κοιμάται εδώ για 1000 χιλιοστά του δευτερολέπτου και μετά τον επόμενο guru1 και ξανά τον guru1, το νήμα κοιμάται για 1000 χιλιοστά του δευτερολέπτου, οπότε παίρνουμε τον guru2 και μετά τον guru1.

Περίληψη:

Σε αυτό το σεμινάριο, είδαμε εφαρμογές πολλαπλών νημάτων σε Java και πώς να χρησιμοποιούμε μονή και πολλαπλά νήματα.

  • Στο multithreading, οι χρήστες δεν αποκλείονται καθώς τα νήματα είναι ανεξάρτητα και μπορούν να εκτελούν πολλαπλές λειτουργίες ταυτόχρονα
  • Τα διάφορα στάδια του κύκλου ζωής του νήματος είναι,
    • Δυνατότητα λειτουργίας
    • Τρέξιμο
    • Αναμονή
  • Μάθαμε επίσης για το συγχρονισμό μεταξύ νημάτων, που βοηθούν την εφαρμογή να λειτουργεί ομαλά.
  • Το Multithreading διευκολύνει πολλές περισσότερες εργασίες εφαρμογής.

Γειά σου! Σε αυτό το άρθρο, θα σας παρουσιάσω εν συντομία τις διεργασίες, τα νήματα και τα βασικά του προγραμματισμού πολλαπλών νημάτων στην Java.
Η πιο προφανής εφαρμογή του multithreading είναι ο προγραμματισμός διεπαφής. Το Multithreading είναι απαραίτητο όταν θέλετε το GUI να συνεχίσει να ανταποκρίνεται στα δεδομένα εισόδου του χρήστη ενώ εκτελείται κάποια επεξεργασία. Για παράδειγμα, το νήμα που είναι υπεύθυνο για τη διεπαφή μπορεί να περιμένει την ολοκλήρωση ενός άλλου νήματος για τη λήψη ενός αρχείου από το Διαδίκτυο και αυτή τη στιγμή να εμφανίσει κάποιο κινούμενο σχέδιο ή να ενημερώσει τη γραμμή προόδου. Επιπλέον, μπορεί να σταματήσει τη λήψη ενός αρχείου από το νήμα, εάν πατήθηκε το κουμπί "ακύρωσης".

Ένας άλλος δημοφιλής και, ίσως, ένας από τους πιο σκληροπυρηνικούς τομείς εφαρμογής για multithreading είναι τα παιχνίδια. Στα παιχνίδια, διάφορα νήματα μπορεί να είναι υπεύθυνα για την εργασία με το δίκτυο, κινούμενα σχέδια, υπολογισμούς φυσικής κ.λπ.

Ας αρχίσουμε. Πρώτον, για τις διαδικασίες.

Διαδικασίες

Μια διαδικασία είναι μια συλλογή κώδικα και δεδομένων που μοιράζονται έναν κοινό χώρο εικονικών διευθύνσεων. Τις περισσότερες φορές, ένα πρόγραμμα αποτελείται από μία διαδικασία, αλλά υπάρχουν εξαιρέσεις (για παράδειγμα, το πρόγραμμα περιήγησης Chrome δημιουργεί μια ξεχωριστή διαδικασία για κάθε καρτέλα, η οποία του παρέχει ορισμένα πλεονεκτήματα, όπως οι καρτέλες ανεξάρτητες μεταξύ τους). Οι διεργασίες είναι απομονωμένες μεταξύ τους, επομένως η άμεση πρόσβαση στη μνήμη μιας άλλης διαδικασίας είναι αδύνατη (η αλληλεπίδραση μεταξύ των διεργασιών πραγματοποιείται με τη χρήση ειδικών μέσων).

Για κάθε διεργασία, το ΛΣ δημιουργεί έναν λεγόμενο «εικονικό χώρο διευθύνσεων» στον οποίο η διεργασία έχει άμεση πρόσβαση. Ο χώρος αυτός ανήκει στη διαδικασία, περιέχει μόνο τα δεδομένα της και είναι πλήρως στη διάθεσή του. Το λειτουργικό σύστημα είναι υπεύθυνο για τον τρόπο προβολής του εικονικού χώρου της διαδικασίας φυσική μνήμη.

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

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

Ρεύματα

Ένα νήμα είναι μία μονάδα εκτέλεσης κώδικα. Κάθε νήμα εκτελεί διαδοχικά τις εντολές της διεργασίας στην οποία ανήκει, παράλληλα με άλλα νήματα αυτής της διεργασίας.

Η φράση "παράλληλα με άλλα νήματα" πρέπει να συζητηθεί χωριστά. Είναι γνωστό ότι υπάρχει μία μονάδα εκτέλεσης ανά πυρήνα επεξεργαστή κάθε δεδομένη στιγμή. Δηλαδή, ένας επεξεργαστής μονού πυρήνα μπορεί να επεξεργάζεται εντολές μόνο διαδοχικά, μία κάθε φορά (σε μια απλοποιημένη περίπτωση). Ωστόσο, η εκτέλεση πολλαπλών παράλληλων νημάτων είναι επίσης δυνατή σε συστήματα με επεξεργαστές ενός πυρήνα. Σε αυτήν την περίπτωση, το σύστημα θα εναλλάσσεται περιοδικά μεταξύ των νημάτων, επιτρέποντας εναλλάξ να εκτελεστεί το ένα ή το άλλο νήμα. Αυτό το σχήμα ονομάζεται ψευδοπαραλληλισμός. Το σύστημα θυμάται την κατάσταση (πλαίσιο) κάθε νήματος πριν μεταβεί σε άλλο νήμα και το επαναφέρει όταν το νήμα επιστρέψει στην εκτέλεση. Το περιβάλλον νήματος περιλαμβάνει παραμέτρους όπως η στοίβα, ένα σύνολο τιμών καταχωρητή επεξεργαστή και τη διεύθυνση εκτελέσιμη εντολήΚαι ούτω καθεξής…

Με απλά λόγια, με την εκτέλεση ψευδο-παράλληλων νημάτων, ο επεξεργαστής τρέχει ανάμεσα στην εκτέλεση πολλών νημάτων, εκτελώντας μέρος του καθενός από αυτά με τη σειρά του.

Έτσι φαίνεται:

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

Το γεγονός ότι οι εντολές από παράλληλα νήματα εκτελούνται αναμεμειγμένα μπορεί να οδηγήσει σε διενέξεις πρόσβασης δεδομένων σε ορισμένες περιπτώσεις. Το επόμενο άρθρο θα αφιερωθεί στα προβλήματα της αλληλεπίδρασης νημάτων, αλλά προς το παρόν θα μιλήσουμε για τον τρόπο εκκίνησης των νημάτων στην Java...

Τρέχοντα νήματα

Κάθε διεργασία έχει τουλάχιστον ένα τρέχον νήμα. Το νήμα από το οποίο ξεκινά η εκτέλεση του προγράμματος ονομάζεται κύριο νήμα. Στην Java, μετά τη δημιουργία μιας διεργασίας, η εκτέλεση του κύριου νήματος ξεκινά με τη μέθοδο main(). Στη συνέχεια, όπως είναι απαραίτητο, σε σημεία που καθορίζονται από τον προγραμματιστή, και όταν πληρούνται οι προϋποθέσεις που καθορίζονται από αυτόν, εκτοξεύονται άλλα πλευρικά νήματα.

Στην Java, ένα νήμα αναπαρίσταται ως αντικείμενο καταγωγής της κλάσης Thread. Αυτή η κλάση περικλείει τυπικούς μηχανισμούς σπειρώματος.

Υπάρχουν δύο τρόποι για να ξεκινήσετε ένα νέο νήμα:

Μέθοδος 1
Δημιουργήστε ένα αντικείμενο της κλάσης Thread, περνώντας το στον κατασκευαστή κάτι που υλοποιεί τη διεπαφή Runnable. Αυτή η διεπαφή περιέχει τη μέθοδο run(), η οποία θα εκτελεστεί σε ένα νέο νήμα. Η εκτέλεση ενός νήματος θα ολοκληρωθεί όταν ολοκληρωθεί η μέθοδος run() του.

Μοιάζει με αυτό:

Κλάση SomeThing //Κάτι που υλοποιεί τη διεπαφή Runnable implements Runnable //(που περιέχει τη μέθοδο run()) ( public void run() //Αυτή η μέθοδος θα εκτελεστεί σε ένα πλευρικό νήμα ( System.out.println("Hello from the πλαϊνό νήμα!") ;) ) δημόσια κλάση Πρόγραμμα //Κλάση με τη μέθοδο main() ( στατικό SomeThing mThing; //mThing είναι ένα αντικείμενο μιας κλάσης που υλοποιεί τη διεπαφή Runnable public static void main(String args) ( mThing = new SomeThing(); ;

Για να συντομεύσετε περαιτέρω τον κώδικα, μπορείτε να περάσετε ένα αντικείμενο μιας ανώνυμης εσωτερικής κλάσης που υλοποιεί τη διεπαφή Runnable στον κατασκευαστή της κλάσης Thread:

Public class Program //Class με τη μέθοδο main(). ( public static void main(String args) ( //Create a thread Thread myThready = new Thread(new Runnable() ( public void run() //Αυτή η μέθοδος θα εκτελεστεί σε ένα πλευρικό νήμα ( System.out.println("Hello από το πλαϊνό νήμα) νήμα!"); ) )); myThready.start(); //Ξεκινήστε ένα νήμα System.out.println("Το κύριο νήμα ολοκληρώθηκε..."); ) )

Μέθοδος 2
Δημιουργήστε έναν απόγονο της κλάσης Thread και παρακάμψτε τη μέθοδο run():

Η κλάση AffableThread επεκτείνει το νήμα ( @Override public void run() //Αυτή η μέθοδος θα εκτελεστεί σε ένα πλευρικό νήμα ( System.out.println("Hello from the side thread!"); ) ) δημόσια τάξη Πρόγραμμα ( static AffableThread mSecondThread; public static void main(String args) ( mSecondThread = new AffableThread(); //Δημιουργία νήματος mSecondThread.start(); //Έναρξη νήματος System.out.println("Το κύριο νήμα έληξε..."); ) )

Στο παραπάνω παράδειγμα, ένα άλλο νήμα δημιουργείται και ξεκινά στη μέθοδο main(). Είναι σημαντικό να σημειωθεί ότι μετά την κλήση της μεθόδου mSecondThread.start(), το κύριο νήμα συνεχίζει την εκτέλεσή του χωρίς να περιμένει να ολοκληρωθεί το νήμα που δημιούργησε. Και αυτές οι οδηγίες που έρχονται μετά την κλήση της μεθόδου start() θα εκτελεστούν παράλληλα με τις οδηγίες του νήματος mSecondThread.

Για να δείξουμε την παράλληλη λειτουργία των νημάτων, ας εξετάσουμε ένα πρόγραμμα στο οποίο δύο νήματα διαφωνούν για το φιλοσοφικό ερώτημα "ποιο ήρθε πρώτο, το αυγό ή το κοτόπουλο;" Το κύριο νήμα είναι σίγουρο ότι το κοτόπουλο ήρθε πρώτο, το οποίο θα αναφέρει κάθε δευτερόλεπτο. Το δεύτερο νήμα θα διαψεύσει τον αντίπαλό του μία φορά το δευτερόλεπτο. Η διαμάχη θα διαρκέσει συνολικά 5 δευτερόλεπτα. Νικητής θα είναι το ρεύμα που θα πει τελευταίος την απάντησή του σε αυτό το, χωρίς αμφιβολία, φλέγον φιλοσοφικό ερώτημα. Το παράδειγμα χρησιμοποιεί λειτουργίες που δεν έχουν ακόμη συζητηθεί (isAlive() sleep() and join()). Δίνονται σχόλια σχετικά με αυτά και θα συζητηθούν λεπτομερέστερα περαιτέρω.

Η κλάση EggVoice επεκτείνει το νήμα ( @Override public void run() ( for(int i = 0; i< 5; i++) { try{ sleep(1000); //Приостанавливает поток на 1 секунду }catch(InterruptedException e){} System.out.println("яйцо!"); } //Слово «яйцо» сказано 5 раз } } public class ChickenVoice //Класс с методом main() { static EggVoice mAnotherOpinion; //Побочный поток public static void main(String args) { mAnotherOpinion = new EggVoice(); //Создание потока System.out.println("Спор начат..."); mAnotherOpinion.start(); //Запуск потока for(int i = 0; i < 5; i++) { try{ Thread.sleep(1000); //Приостанавливает поток на 1 секунду }catch(InterruptedException e){} System.out.println("курица!"); } //Слово «курица» сказано 5 раз if(mAnotherOpinion.isAlive()) //Если оппонент еще не сказал последнее слово { try{ mAnotherOpinion.join(); //Подождать пока оппонент закончит высказываться. }catch(InterruptedException e){} System.out.println("Первым появилось яйцо!"); } else //если оппонент уже закончил высказываться { System.out.println("Первой появилась курица!"); } System.out.println("Спор закончен!"); } } Консоль: Спор начат... курица! яйцо! яйцо! курица! яйцо! курица! яйцо! курица! яйцо! курица! Первой появилась курица! Спор закончен!

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

Τώρα λίγο για την ολοκλήρωση των διαδικασιών...

Τερματισμός διαδικασίας και δαίμονες

Στην Java, μια διεργασία τερματίζεται όταν τερματιστεί το τελευταίο της νήμα. Ακόμα κι αν η μέθοδος main() έχει ήδη ολοκληρωθεί, αλλά τα νήματα που δημιούργησε εξακολουθούν να εκτελούνται, το σύστημα θα περιμένει να ολοκληρωθούν.

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

Η δήλωση ενός νήματος ως δαίμονα είναι αρκετά απλή - πρέπει να καλέσετε τη μέθοδο του πριν ξεκινήσετε το νήμα setDaemon(αληθές);
Μπορείτε να ελέγξετε αν ένα νήμα είναι δαίμονας καλώντας τη μέθοδο του boolean isDaemon();

Τερματιστικά νήματα

Υπάρχουν (υπήρχαν) εγκαταστάσεις στην Java για αναγκαστική διακοπήροή. Συγκεκριμένα, η μέθοδος Thread.stop() τερματίζει το νήμα αμέσως μετά την εκτέλεση. Ωστόσο, αυτή η μέθοδος, καθώς και η Thread.suspend(), η οποία αναστέλλει ένα νήμα και η Thread.resume(), η οποία συνεχίζει την εκτέλεση ενός νήματος, έχουν καταργηθεί και η χρήση τους πλέον αποθαρρύνεται ιδιαίτερα. Γεγονός είναι ότι ένα νήμα μπορεί να «σκοτωθεί» κατά την εκτέλεση μιας λειτουργίας, η διακοπή της οποίας η μέση πρόταση θα αφήσει κάποιο αντικείμενο σε λάθος κατάσταση, το οποίο θα οδηγήσει στην εμφάνιση ενός δυσκολοπιάσιμου και τυχαίου σφάλματος.

Αντί να αναγκαστεί ένα νήμα να τερματίσει, χρησιμοποιείται ένα σχήμα στο οποίο κάθε νήμα είναι υπεύθυνο για τον δικό του τερματισμό. Ένα νήμα μπορεί να σταματήσει είτε όταν ολοκληρώσει την εκτέλεση της μεθόδου run() (main() για το κύριο νήμα) είτε με ένα σήμα από άλλο νήμα. Επιπλέον, το πώς να αντιδράσετε σε ένα τέτοιο σήμα είναι, και πάλι, θέμα της ίδιας της ροής. Αφού το λάβει, το νήμα μπορεί να εκτελέσει ορισμένες λειτουργίες και να ολοκληρώσει την εκτέλεση ή μπορεί να το αγνοήσει εντελώς και να συνεχίσει την εκτέλεση. Η περιγραφή της αντίδρασης στο σήμα τερματισμού νήματος στηρίζεται στους ώμους του προγραμματιστή.

Η Java διαθέτει έναν ενσωματωμένο μηχανισμό ειδοποίησης νημάτων που ονομάζεται Διακοπή, τον οποίο θα εξετάσουμε σύντομα, αλλά πρώτα ρίξτε μια ματιά στον ακόλουθο κώδικα:

Ο Incremenator είναι ένα νήμα που προσθέτει ή αφαιρεί ένα από την τιμή της στατικής μεταβλητής Program.mValue κάθε δευτερόλεπτο. Το Incremenator περιέχει δύο ιδιωτικά πεδία - mIsIncrement και mFinish. Η ενέργεια που εκτελείται καθορίζεται από τη μεταβλητή Boolean mIsIncrement - εάν είναι αληθής, τότε γίνεται η πρόσθεση ενός, διαφορετικά η αφαίρεση. Και το νήμα τελειώνει όταν η τιμή mFinish γίνει αληθής.

Το Class Incremenator επεκτείνει το νήμα ( //Σχετικά με την πτητική λέξη-κλειδί - ακριβώς κάτω από το ιδιωτικό πτητικό boolean mIsIncrement = true; private volatile boolean mFinish = false; public void changeAction() //Αλλάζει την ενέργεια στο αντίθετο ( mIsIncrement = !mIsIncrement; ) publicid finish( ) //Εκκινεί την ολοκλήρωση του νήματος ( mFinish = true; ) @Override public void run() ( do ( if(!mFinish) //Ελέγχει την ανάγκη τερματισμού ( if(mIsIncrement) Program.mValue++; / /Increment else Program.mValue- //Μείωση //Έξοδος της τρέχουσας τιμής της μεταβλητής System.out.print(Program.mValue + " " else return (Νήμα.sleep). ); //Αντικείμενο πλευρικού νήματος public static void main(String args ) ( mInc = new Incremenator(); //Δημιουργία νήματος System.out.print("Value = "); mInc.start(); //Ξεκινήστε το νήμα //Αλλάξτε την ενέργεια αύξησης τρεις φορές //με διάστημα i*2 δευτερολέπτων για(int i = 1; i<= 3; i++) { try{ Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек. }catch(InterruptedException e){} mInc.changeAction(); //Переключение действия } mInc.finish(); //Инициация завершения побочного потока } } Консоль: Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

Μπορείτε να αλληλεπιδράσετε με τη ροή χρησιμοποιώντας τη μέθοδο changeAction() (για να αλλάξετε την αφαίρεση σε πρόσθεση και αντίστροφα) και τη μέθοδο finish() (για να τερματίσετε τη ροή).

Η λέξη-κλειδί volatile χρησιμοποιήθηκε στη δήλωση των μεταβλητών mIsIncrement και mFinish. Πρέπει να χρησιμοποιείται για μεταβλητές που χρησιμοποιούνται από διαφορετικά νήματα. Αυτό συμβαίνει επειδή η τιμή μιας μη πτητικής μεταβλητής μπορεί να αποθηκευτεί στην κρυφή μνήμη ξεχωριστά για κάθε νήμα και η τιμή από αυτήν την προσωρινή μνήμη μπορεί να είναι διαφορετική για κάθε νήμα. Η δήλωση μιας μεταβλητής με την πτητική λέξη-κλειδί απενεργοποιεί αυτήν την προσωρινή αποθήκευση για αυτήν και όλα τα αιτήματα προς τη μεταβλητή θα αποστέλλονται απευθείας στη μνήμη.

Αυτό το παράδειγμα δείχνει πώς μπορείτε να οργανώσετε την επικοινωνία μεταξύ των νημάτων. Ωστόσο, υπάρχει ένα πρόβλημα με αυτήν την προσέγγιση για την ολοκλήρωση ενός νήματος - ο Incremenator ελέγχει την τιμή του πεδίου mFinish μία φορά ανά δευτερόλεπτο, επομένως μπορεί να μεσολαβήσει έως και ένα δευτερόλεπτο από την εκτέλεση της μεθόδου finish() και το νήμα στην πραγματικότητα κατάληξη. Θα ήταν υπέροχο εάν, κατά τη λήψη ενός σήματος από το εξωτερικό, η μέθοδος sleep() επέστρεφε την εκτέλεση και το νήμα άρχιζε αμέσως να τερματίζεται. Για να χειριστείτε αυτό το σενάριο, υπάρχει ένα ενσωματωμένο εργαλείο ειδοποίησης νήματος που ονομάζεται Διακοπή.

ΔΙΑΚΟΠΗ

Η κλάση Thread περιέχει ένα κρυφό πεδίο Boolean, παρόμοιο με το πεδίο mFinish στο πρόγραμμα Incremenator, που ονομάζεται σημαία διακοπής. Αυτή η σημαία μπορεί να οριστεί καλώντας τη μέθοδο interrupt() του νήματος. Υπάρχουν δύο τρόποι για να ελέγξετε εάν αυτή η σημαία έχει οριστεί. Ο πρώτος τρόπος είναι να καλέσετε τη μέθοδο bool isInterrupted() του αντικειμένου νήματος, ο δεύτερος είναι να καλέσετε τη μέθοδο static bool Thread.interrupted(). Η πρώτη μέθοδος επιστρέφει την κατάσταση της σημαίας διακοπής και αφήνει τη σημαία ανέγγιχτη. Η δεύτερη μέθοδος επιστρέφει την κατάσταση της σημαίας και την επαναφέρει. Σημειώστε ότι η Thread.interrupted() είναι μια στατική μέθοδος της κλάσης Thread και η κλήση της επιστρέφει την τιμή της σημαίας διακοπής του νήματος από το οποίο κλήθηκε. Επομένως, αυτή η μέθοδος καλείται μόνο μέσα από ένα νήμα και επιτρέπει στο νήμα να ελέγξει την κατάσταση διακοπής του.

Ας επιστρέψουμε λοιπόν στο πρόγραμμά μας. Ο μηχανισμός διακοπής θα μας επιτρέψει να λύσουμε το πρόβλημα με το νήμα να κοιμάται. Οι μέθοδοι που αναστέλλουν την εκτέλεση ενός νήματος, όπως η sleep(), η wait() και η join(), έχουν μια ιδιαιτερότητα - εάν καλείται η μέθοδος interrupt() του νήματος ενώ εκτελούνται, θα ρίξουν μια InterruptedException χωρίς να περιμένουν το τέλος του τάιμ άουτ.

Ας επεξεργαστούμε ξανά το πρόγραμμα Incremenator - τώρα, αντί να τερματίσουμε το νήμα χρησιμοποιώντας τη μέθοδο finish(), θα χρησιμοποιήσουμε την τυπική μέθοδο interrupt(). Και αντί να ελέγξουμε τη σημαία mFinish, θα καλέσουμε τη μέθοδο bool Thread.interrupted();
Έτσι θα μοιάζει η κλάση Incremenator μετά την προσθήκη υποστήριξης διακοπής:

Το Class Incremenator επεκτείνει το νήμα ( private volatile boolean mIsIncrement = true; public void changeAction() //Ανστρέφει την ενέργεια ( mIsIncrement = !mIsIncrement; ) @Override public void run() ( do ( if(!Thread.interrupted()) / / Έλεγχος διακοπής ( if(mIsIncrement) Program.mValue++; //Increment else Program.mValue--; //Decrement //Έξοδος της τρέχουσας τιμής της μεταβλητής System.out.print(Program.mValue + " "); ) αλλιώς επιστροφή / / /Τερματισμός του νήματος try( Thread.sleep(1000); //Αναστολή του νήματος για 1 δευτερόλεπτο. //Μεταβλητή, την οποία ο αυξητικός παράγοντας λειτουργεί σε δημόσιο στατικό int mValue = 0 στατικό αντικείμενο Incremenator mInc; out.print("Τιμή = " ); Εγώ<= 3; i++) { try{ Thread.sleep(i*2*1000); //Ожидание в течении i*2 сек. }catch(InterruptedException e){} mInc.changeAction(); //Переключение действия } mInc.interrupt(); //Прерывание побочного потока } } Консоль: Значение = 1 2 1 0 -1 -2 -1 0 1 2 3 4

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

Σημειώστε ότι οι μέθοδοι sleep() και join() είναι τυλιγμένες σε δομές try-catch. Αυτή είναι απαραίτητη προϋπόθεση για να λειτουργήσουν αυτές οι μέθοδοι. Ο κωδικός που τους καλεί πρέπει να πιάσει το InterruptedException που ρίχνουν όταν διακόπτονται ενώ περιμένουν.

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

Μέθοδος Thread.sleep().

Το Thread.sleep() είναι μια στατική μέθοδος της κλάσης Thread που αναστέλλει την εκτέλεση του νήματος στο οποίο κλήθηκε. Ενώ εκτελείται η μέθοδος sleep(), το σύστημα σταματά να εκχωρεί χρόνο επεξεργαστή στο νήμα, κατανέμοντάς τον μεταξύ άλλων νημάτων. Η μέθοδος sleep() μπορεί να εκτελεστεί είτε για ένα καθορισμένο χρονικό διάστημα (χιλιοστά του δευτερολέπτου ή νανοδευτερόλεπτα) είτε μέχρι να σταματήσει από μια διακοπή (στην περίπτωση αυτή θα δημιουργήσει μια InterruptedException).

Thread.sleep(1500); //Περιμένει ενάμιση δευτερόλεπτο Thread.sleep(2000, 100); //Περιμένει 2 δευτερόλεπτα και 100 νανοδευτερόλεπτα

Παρόλο που η μέθοδος sleep() μπορεί να πάρει νανοδευτερόλεπτα για να περιμένει, δεν πρέπει να ληφθεί σοβαρά υπόψη. Σε πολλά συστήματα, ο χρόνος αναμονής εξακολουθεί να στρογγυλοποιείται σε χιλιοστά του δευτερολέπτου ή και δεκάδες.

μέθοδος yield().

Η στατική μέθοδος Thread.yield() αναγκάζει τον επεξεργαστή να μεταβεί στην επεξεργασία άλλων νημάτων στο σύστημα. Η μέθοδος μπορεί να είναι χρήσιμη, για παράδειγμα, όταν ένα νήμα περιμένει να συμβεί ένα συμβάν και είναι απαραίτητο να ελέγχετε για την εμφάνισή του όσο το δυνατόν συχνότερα. Σε αυτήν την περίπτωση, μπορείτε να βάλετε τον έλεγχο συμβάντος και τη μέθοδο Thread.yield() σε έναν βρόχο:

//Αναμονή για να φτάσει ένα μήνυμα ενώ(!msgQueue.hasMessages()) //Ενώ δεν υπάρχουν μηνύματα στην ουρά ( Thread.yield(); //Μεταφορά ελέγχου σε άλλα νήματα)

μέθοδο join().

Η Java παρέχει έναν μηχανισμό που επιτρέπει σε ένα νήμα να περιμένει για να ολοκληρωθεί η εκτέλεση ενός άλλου νήματος. Για αυτό χρησιμοποιείται η μέθοδος join(). Για παράδειγμα, για να περιμένει το κύριο νήμα για να ολοκληρωθεί το πλευρικό νήμα myThready, πρέπει να εκδώσετε τη δήλωση myThready.join() στο κύριο νήμα. Μόλις το myThready βγει, η μέθοδος join() επιστρέφει και το κύριο νήμα μπορεί να συνεχίσει να εκτελείται.

Η μέθοδος join() έχει μια υπερφόρτωση που απαιτεί ένα χρονικό όριο ως παράμετρο. Σε αυτήν την περίπτωση, η join() επιστρέφει είτε όταν τελειώσει το νήμα που περιμένει είτε όταν λήξει το χρονικό όριο. Όπως η μέθοδος Thread.sleep(), η μέθοδος join μπορεί να περιμένει χιλιοστά του δευτερολέπτου και νανοδευτερόλεπτα - τα ορίσματα είναι τα ίδια.

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

Thinker brain = new Thinker(); //Thinker είναι απόγονος της κλάσης Thread. brain.start(); //Ξεκινήστε να "σκέφτεστε". do ( mThinkIndicator.refresh(); //mThinkIndicator - κινούμενη εικόνα. try( brain.join(250); //Περιμένετε ένα τέταρτο του δευτερολέπτου για να τελειώσει η σκέψη. ) catch(InterruptedException e)() ) while(brain .ειναι ΖΩΝΤΑΝΟΣ()); //Ενώ ο εγκέφαλος σκέφτεται... //Ο εγκέφαλος έχει τελειώσει τη σκέψη (υπάρχει χειροκρότημα).

Σε αυτό το παράδειγμα, το εγκεφαλικό νήμα σκέφτεται κάτι και υποτίθεται ότι χρειάζεται πολύς χρόνος για να το κάνει. Το κύριο νήμα τον περιμένει για ένα τέταρτο του δευτερολέπτου και, αν αυτή η φορά για σκέψη δεν είναι αρκετή, ενημερώνει τον "δείκτη σκέψης" (κάποια κινούμενη εικόνα). Ως αποτέλεσμα, ενώ σκέφτεται, ο χρήστης παρατηρεί μια ένδειξη της διαδικασίας σκέψης στην οθόνη, η οποία τον ενημερώνει ότι οι ηλεκτρονικοί εγκέφαλοι είναι απασχολημένοι με κάτι.

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

Κάθε νήμα στο σύστημα έχει τη δική του προτεραιότητα. Προτεραιότητα είναι κάποιος αριθμός στο αντικείμενο νήματος, υψηλότερη τιμή του οποίου σημαίνει υψηλότερη προτεραιότητα. Το σύστημα εκτελεί πρώτα νήματα υψηλότερης προτεραιότητας και τα νήματα χαμηλότερης προτεραιότητας λαμβάνουν χρόνο CPU μόνο όταν τα πιο προνομιούχα αντίστοιχά τους είναι σε αδράνεια.

Μπορείτε να εργαστείτε με προτεραιότητες νημάτων χρησιμοποιώντας δύο λειτουργίες:

void setPriority (int priority)– ορίζει την προτεραιότητα του νήματος.
Πιθανές τιμές προτεραιότητας είναι MIN_PRIORITY, NORM_PRIORITY και MAX_PRIORITY.

int getPriority()– παίρνει προτεραιότητα νήματος.

Μερικές χρήσιμες μέθοδοι της κλάσης Thread

Αυτό είναι πρακτικά όλο. Τέλος, ακολουθούν ορισμένες χρήσιμες μέθοδοι για την εργασία με ροές.

boolean isAlive()- επιστρέφει true εάν η myThready() εκτελείται και false εάν το νήμα δεν έχει ξεκινήσει ακόμη ή έχει τερματιστεί.

setName (Όνομα νήματος συμβολοσειράς)– Καθορίζει το όνομα της ροής.
Συμβολοσειρά getName()– Λαμβάνει το όνομα του νήματος.
Το όνομα του νήματος είναι μια συμβολοσειρά που σχετίζεται με αυτό, η οποία σε ορισμένες περιπτώσεις βοηθά να κατανοήσουμε ποιο νήμα εκτελεί μια ενέργεια. Μερικές φορές αυτό μπορεί να είναι χρήσιμο.

static Thread Thread.currentThread()- μια στατική μέθοδος που επιστρέφει το αντικείμενο του νήματος στο οποίο κλήθηκε.

long getId()– επιστρέφει το αναγνωριστικό νήματος. Το αναγνωριστικό είναι ένας μοναδικός αριθμός που εκχωρείται στη ροή.

συμπέρασμα

Σημειώνω ότι το άρθρο δεν μιλά για όλες τις αποχρώσεις του προγραμματισμού πολλαπλών νημάτων. Και ο κώδικας που δίνεται στα παραδείγματα δεν έχει κάποιες αποχρώσεις για να είναι απόλυτα σωστός. Συγκεκριμένα, τα παραδείγματα δεν χρησιμοποιούν συγχρονισμό. Ο συγχρονισμός νημάτων είναι ένα θέμα χωρίς το οποίο δεν θα μπορείτε να προγραμματίσετε κατάλληλες εφαρμογές πολλαπλών νημάτων. Μπορείτε να διαβάσετε γι 'αυτό, για παράδειγμα, στο βιβλίο "Java Concurrency in Practice" ή

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

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

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

Δημιουργία νημάτων

Οι δημιουργοί της Java πρόσφεραν ευγενικά δύο δυνατότητες για τη δημιουργία νημάτων: την υλοποίηση μιας διεπαφής και την επέκταση μιας κλάσης. Η επέκταση μιας κλάσης είναι ένας τρόπος κληρονομιάς μεθόδων και μεταβλητών της γονικής κλάσης. Σε αυτήν την περίπτωση, μπορείτε να κληρονομήσετε μόνο από μία γονική κλάση. Αυτός ο περιορισμός στην Java μπορεί να ξεπεραστεί με την υλοποίηση μιας διεπαφής, η οποία είναι ο πιο συνηθισμένος τρόπος δημιουργίας νημάτων. (Σημειώστε ότι η μέθοδος κληρονομικότητας επιτρέπει στην κλάση να εκτελείται μόνο ως νήμα. Αυτό επιτρέπει μόνο στην κλάση να εκτελέσει start(), κ.λπ.).

Οι διεπαφές επιτρέπουν στους προγραμματιστές να θέσουν τα θεμέλια μιας ενιαίας κλάσης. Χρησιμοποιούνται για την ανάπτυξη απαιτήσεων για ένα σύνολο κλάσεων που πρόκειται να εφαρμοστούν. Η διεπαφή θέτει τους κανόνες. Τα διαφορετικά σύνολα κλάσεων που υλοποιούν τη διεπαφή πρέπει να ακολουθούν αυτούς τους κανόνες.

Υπάρχουν κάποιες διαφορές μεταξύ μιας κλάσης και μιας διεπαφής. Πρώτον, μια διεπαφή μπορεί να περιέχει μόνο αφηρημένες μεθόδους ή/και στατικές τελικές μεταβλητές (σταθερές). Οι κλάσεις, από την άλλη πλευρά, μπορούν να υλοποιήσουν μεθόδους και να περιέχουν μεταβλητές που δεν λειτουργούν ως σταθερές. Δεύτερον, μια διεπαφή δεν μπορεί να εφαρμόσει καμία μέθοδο. Μια κλάση που υλοποιεί μια διεπαφή πρέπει να εφαρμόσει όλες τις μεθόδους που περιγράφονται στη διεπαφή. Μια διεπαφή έχει τη δυνατότητα να επεκταθεί από άλλες διεπαφές και (σε ​​αντίθεση με τις κλάσεις) μπορεί να επεκταθεί από πολλαπλές διεπαφές. Επιπλέον, δεν μπορεί να δημιουργηθεί ένα στιγμιότυπο διεπαφής χρησιμοποιώντας τον νέο τελεστή. για παράδειγμα, Runnable a = new Runnable(); δεν επιτρέπεται.

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


εισαγωγή java.lang.*;
δημόσια τάξη Ο μετρητής επεκτείνει το νήμα (
public void run() (
....
}
}

Το παραπάνω παράδειγμα δημιουργεί μια νέα κλάση Counter που επεκτείνει την κλάση Thread και παρακάμπτει τη μέθοδο Thread.run() για την υλοποίησή της. Η μέθοδος run() είναι όπου συμβαίνει όλη η εργασία της κλάσης Counter ως νήμα. Η ίδια κλάση μπορεί να δημιουργηθεί με την υλοποίηση της διεπαφής Runnable:


εισαγωγή java.lang.*;
δημόσια κλάση Counter υλοποιεί Runnable (
Νήμα T;
public void run() (
....
}
}

Αυτό υλοποιεί τη μέθοδο abstract run(), η οποία περιγράφεται στη διεπαφή Runnable. Σημειώστε ότι έχουμε μια παρουσία της κλάσης Thread, μια μεταβλητή της κλάσης Counter. Η μόνη διαφορά μεταξύ των δύο μεθόδων είναι ότι η υλοποίηση Runnable είναι πιο ευέλικτη για τη δημιουργία μιας κλάσης Counter. Στο παράδειγμα που περιγράφεται παραπάνω, είναι δυνατή η επέκταση της κλάσης Counter εάν είναι απαραίτητο. Οι περισσότερες κλάσεις που πρέπει να εκτελούνται ως νήματα εφαρμόζουν το Runnable επειδή πιθανότατα μπορούν να επεκτείνουν τη λειτουργικότητά τους σε άλλη κλάση.

Μην νομίζετε ότι η διεπαφή Runnable λειτουργεί πραγματικά όταν ξεκινά το νήμα. Αυτή είναι απλώς μια κλάση που δημιουργήθηκε για να δώσει μια ιδέα για την κλάση Thread. Στην πραγματικότητα, είναι μια πολύ μικρή διεπαφή που περιέχει μόνο μία αφηρημένη μέθοδο. Αυτή είναι η περιγραφή της διεπαφής Runnable που λαμβάνεται από τους πηγαίους κώδικες Java:


πακέτο java.lang;
δημόσια διεπαφή με δυνατότητα εκτέλεσης (
δημόσια αφηρημένη void run();
}

Αυτό είναι το μόνο που υπάρχει στη διεπαφή Runnable. Μια διεπαφή είναι απλώς μια περιγραφή που πρέπει να εφαρμόσουν οι κλάσεις. Έτσι, το Runnable προκαλεί μόνο την εκτέλεση της μεθόδου run(). Ως αποτέλεσμα, το μεγαλύτερο μέρος της εργασίας βασίζεται στην κλάση Thread. Μια πιο προσεκτική ματιά στην τάξη Thread θα σας δώσει μια ιδέα για το τι πραγματικά συμβαίνει:


δημόσια κλάση Thread υλοποιεί Runnable (
...
public void run() (
αν (στόχος != μηδενικός) (
target.run();
}
}
...
}

Στο τμήμα κώδικα που παρουσιάστηκε παραπάνω, μπορείτε να δείτε ότι η κλάση Thread υλοποιεί επίσης τη διεπαφή Runnable. Η Thread.run() ελέγχει για να βεβαιωθεί ότι η κλάση (η κλάση που εκτελείται ως νήμα) δεν είναι null πριν εκτελέσει τη μέθοδο run(). Όταν συμβεί αυτό, η μέθοδος run() θα ξεκινήσει το νήμα.

Ξεκινήστε και σταματήστε

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

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

Σε αυτήν την περίπτωση, η κλάση CounterThread αναγκάστηκε να εφαρμόσει τη διεπαφή Runnnable προκειμένου να επεκτείνει περαιτέρω την κλάση Applet. Όλες οι μικροεφαρμογές ξεκινούν την εργασία τους με τη μέθοδο init(), η μεταβλητή Cout αρχικοποιείται στο μηδέν και δημιουργείται ένα νέο αντικείμενο κλάσης Thread. Περνώντας αυτό στον κατασκευαστή της κλάσης Thread, με αυτόν τον τρόπο το νέο νήμα θα γνωρίζει ποιο αντικείμενο εκτελείται. Σε αυτήν την περίπτωση, πρόκειται για αναφορά στο CounterThread. Αφού δημιουργηθεί το νήμα, πρέπει να ξεκινήσει. Καλούμε τη μέθοδο start(), η οποία με τη σειρά της καλεί τη μέθοδο run() του αντικειμένου CounterThread, δηλαδή CounterThread.run(). Η μέθοδος start() θα εκτελεστεί αμέσως και ταυτόχρονα το νήμα θα ξεκινήσει τη δουλειά του. Σημειώστε ότι η μέθοδος run() έχει έναν άπειρο βρόχο. Είναι άπειρο γιατί μόλις εκτελεστεί η μέθοδος run(), το νήμα θα βγει. Η μέθοδος run() θα αυξήσει τη μεταβλητή Count, θα αδράνει για 10 δευτερόλεπτα και θα στείλει ένα αίτημα για ενημέρωση της οθόνης της μικροεφαρμογής.

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

Αναστολή και επανέναρξη

Όταν ένα νήμα διακόπτεται χρησιμοποιώντας τη μέθοδο stop(), δεν μπορεί πλέον να συνεχιστεί χρησιμοποιώντας τη μέθοδο start(), αμέσως μετά την κλήση της μεθόδου stop(), το τρέχον νήμα σκοτώνεται. Αντίθετα, μπορείτε να θέσετε σε παύση την εκτέλεση του νήματος χρησιμοποιώντας τη μέθοδο sleep() για ένα συγκεκριμένο χρονικό διάστημα και, στη συνέχεια, το νήμα θα συνεχίσει να εκτελείται όταν τελειώσει ο χρόνος. Αλλά αυτή δεν είναι η καλύτερη λύση εάν το νήμα πρέπει να ξεκινήσει όταν παρουσιαστεί μια συγκεκριμένη κατάσταση. Για αυτό, χρησιμοποιείται η μέθοδος suspend(), η οποία καθιστά δυνατή την προσωρινή διακοπή της εκτέλεσης ενός νήματος, και η μέθοδος resume(), που επιτρέπει στο νήμα να συνεχίσει την εκτέλεση. Η παρακάτω μικροεφαρμογή είναι μια τροποποίηση της μικροεφαρμογής που δίνεται παραπάνω, αλλά χρησιμοποιώντας τις μεθόδους suspend() και resume():


δημόσια κλάση CounterThread2 επεκτείνει Applet υλοποιεί Runnable
{
Νήμα t;
int Count?
Boolean αναστέλλεται?
δημόσιο boolean mouseDown (Συμβάν e,int x, int y)
{
εάν (σε αναστολή)
t.resume();
αλλού
t.suspend();
suspended = !αναστέλλεται;
επιστροφή αληθινή?
}
...
}

Για να αποθηκεύσετε την τρέχουσα κατάσταση της μικροεφαρμογής, χρησιμοποιείται η μεταβλητή boolean suspended. Ο χαρακτηρισμός των διαφορετικών καταστάσεων της μικροεφαρμογής είναι ένα σημαντικό μέρος, επειδή ορισμένες μέθοδοι ενδέχεται να δημιουργούν εξαιρέσεις εάν καλούνται από λάθος κατάσταση. Για παράδειγμα, εάν ένα νήμα ξεκινήσει και σταματήσει, η κλήση της μεθόδου start() θα δημιουργήσει ένα IllegalThreadStateException.

Σχεδίαση

Η Java έχει έναν χρονοπρογραμματιστή νημάτων που παρακολουθεί όλα τα νήματα που εκτελούνται σε όλα τα προγράμματα και αποφασίζει ποια νήματα πρέπει να ξεκινήσουν και ποια γραμμή κώδικα πρέπει να εκτελεστεί. Υπάρχουν δύο χαρακτηριστικά ενός νήματος με τα οποία ο χρονοπρογραμματιστής προσδιορίζει μια διαδικασία. Το πρώτο, πιο σημαντικό, είναι η προτεραιότητα του νήματος, το άλλο είναι αν το νήμα είναι σημαία δαίμονα. Ο απλούστερος κανόνας του χρονοπρογραμματιστή είναι ότι εάν εκτελούνται μόνο νήματα daemon, τότε η Java Virtual Machine (JVM) θα ξεφορτωθεί. Τα νέα νήματα κληρονομούν προτεραιότητα και σημαία δαίμονα από το νήμα που το δημιούργησε. Ο προγραμματιστής καθορίζει ποιο νήμα πρέπει να εκτελεστεί αναλύοντας την προτεραιότητα όλων των νημάτων. Το νήμα με την υψηλότερη προτεραιότητα επιτρέπεται να εκτελεστεί πριν από τα νήματα με χαμηλότερες προτεραιότητες.

Ο προγραμματιστής μπορεί να είναι δύο τύπων: με και χωρίς προτεραιότητα. Ο προγραμματιστής εκμεταλλεύεται την παροχή συγκεκριμένου χρόνου για όλα τα νήματα που εκτελούνται στο σύστημα. Ο προγραμματιστής αποφασίζει ποιο νήμα είναι επόμενο να τρέξει ή να συνεχίσει μετά από κάποιο σταθερό χρονικό διάστημα. Όταν ξεκινά ένα νήμα, μετά από αυτό το συγκεκριμένο χρονικό διάστημα, το τρέχον νήμα θα τεθεί σε αναστολή και το επόμενο νήμα θα συνεχίσει να λειτουργεί. Ο προγραμματιστής χωρίς προτεραιότητα αποφασίζει ποιο νήμα θα ξεκινήσει και θα εκτελεστεί μέχρι να ολοκληρώσει την εργασία του. Το νήμα έχει τον απόλυτο έλεγχο του συστήματος για όσο διάστημα θέλει. Η μέθοδος yield() μπορεί να χρησιμοποιηθεί για να αναγκάσει τον προγραμματιστή να εκτελέσει ένα άλλο νήμα που περιμένει με τη σειρά του. Ανάλογα με το σύστημα στο οποίο εκτελείται η Java, ο προγραμματιστής μπορεί να είναι είτε με είτε χωρίς προτεραιότητα.

Προτεραιότητες

Ο προγραμματιστής καθορίζει ποιο νήμα πρέπει να εκτελείται με βάση τον αριθμό προτεραιότητας που έχει εκχωρηθεί σε κάθε νήμα. Η προτεραιότητα ενός νήματος μπορεί να πάρει τιμές από 1 έως 10. Από προεπιλογή, η τιμή προτεραιότητας για ένα νήμα είναι Thread.NORM_PRIORITY, η οποία έχει τιμή 5. Δύο άλλες στατικές μεταβλητές είναι επίσης διαθέσιμες: Thread.MIN_PRIORITY, μια τιμή από 1, και Thread.MAX_PRIORITY - 10. Η μέθοδος getPriority() μπορεί να χρησιμοποιηθεί για να πάρει την τρέχουσα τιμή προτεραιότητας του αντίστοιχου νήματος.

Δαίμονες ροές

Αυτά τα νήματα ονομάζονται μερικές φορές "υπηρεσίες", οι οποίες συνήθως εκτελούνται με τη χαμηλότερη προτεραιότητα και παρέχουν βασικές υπηρεσίες σε ένα πρόγραμμα ή προγράμματα όταν η δραστηριότητα του υπολογιστή μειώνεται. Ένα παράδειγμα τέτοιου νήματος είναι ένας συλλέκτης σκουπιδιών. Αυτό το νήμα, που παρέχεται από το JVM, σαρώνει προγράμματα για μεταβλητές που δεν θα χρειαστεί ποτέ ξανά πρόσβαση και ελευθερώνει τους πόρους τους, επιστρέφοντάς τους στο σύστημα. Ένα νήμα μπορεί να γίνει νήμα δαίμονα περνώντας τη δυαδική τιμή αληθινή στη μέθοδο setDaemon(). Εάν είναι false, το νήμα γίνεται κανονικό νήμα χρήστη. Ωστόσο, αυτό πρέπει να γίνει πριν ξεκινήσει το νήμα.

Παράδειγμα προγραμματισμού

Η μικροεφαρμογή που παρουσιάζεται δείχνει την εκτέλεση δύο νημάτων με διαφορετικές προτεραιότητες. Το ένα νήμα εκτελείται με τη χαμηλότερη προτεραιότητα και το άλλο με την υψηλότερη. Τα νήματα αυξάνουν τον μετρητή μέχρι ο μετρητής του νήματος υψηλότερης προτεραιότητας να φτάσει τον μετρητή του νήματος χαμηλότερης προτεραιότητας.

συμπέρασμα

Η χρήση νημάτων σε Java θα επιτρέψει στους προγραμματιστές να επωφεληθούν από την Java στα προγράμματά τους πιο ευέλικτα. Η ευκολία δημιουργίας, διαμόρφωσης και εκτέλεσης νημάτων θα δώσει στους προγραμματιστές Java τη δυνατότητα να αναπτύξουν φορητές και ισχυρές μικροεφαρμογές/εφαρμογές που δεν μπορούν να γίνουν σε άλλες γλώσσες τρίτης γενιάς. Τα νήματα επιτρέπουν σε οποιοδήποτε πρόγραμμα να εκτελεί πολλαπλές εργασίες ως μία. Σε γλώσσες που γνωρίζουν το Διαδίκτυο, όπως η Java, αυτό είναι ένα πολύ σημαντικό εργαλείο.

Σχετικά με τον Συγγραφέα

Ο Donald G. Drake προγραμματίζει σε Java από την κυκλοφορία του alpha. Ο Drake έχει γράψει μικροεφαρμογές για ειδικούς ιστότοπους. Η πιο δημοφιλής έκδοσή του είναι το TickerTape (http://www.netobjective.com/java/TickerTapeInfo.html), το οποίο μπορεί να διαμορφωθεί ευρέως ώστε να ενσωματώνεται σε οποιαδήποτε ιστοσελίδα. Ο Drake είναι κάτοχος πτυχίου Bachelor of Science στην επιστήμη των υπολογιστών από το Πανεπιστήμιο John Carroll. Αυτή τη στιγμή παρακολουθεί μεταπτυχιακό τίτλο σπουδών στο Πανεπιστήμιο DePaul.

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

Το πρώτο πρόβλημα είναι η πρόσβαση σε έναν πόρο από πολλά νήματα. Έχουμε ήδη περιγράψει το πρόβλημα με ένα φτυάρι. Μπορείτε να επεκτείνετε την επιλογή - υπάρχει μια δεξαμενή νερού (με μία βρύση), 25 διψασμένοι ανθρακωρύχοι και 5 κούπες για όλους. Θα πρέπει να συνεννοηθούμε, διαφορετικά μπορεί να ξεκινήσει η δολοφονία. Επιπλέον, είναι απαραίτητο όχι μόνο να διατηρούνται ανέπαφες οι κούπες, αλλά και να οργανώνονται τα πάντα ώστε να μπορούν όλοι να πίνουν. Αυτό εν μέρει εντάσσεται στο πρόβλημα νούμερο δύο.
Το δεύτερο πρόβλημα είναι ο συγχρονισμός αλληλεπίδρασης. Κάποτε μου πρότειναν μια εργασία - να γράψω ένα απλό πρόγραμμα ώστε δύο νήματα να μπορούν να παίζουν πινγκ-πονγκ. Ο ένας γράφει "Ping" και ο άλλος "Pong". Αλλά πρέπει να το κάνουν αυτό ένα προς ένα. Τώρα ας φανταστούμε ότι πρέπει να κάνουμε την ίδια εργασία, αλλά με 4 νήματα - ας παίξουμε ζευγάρι σε ζευγάρι.

Εκείνοι. Η διατύπωση των προβλημάτων είναι αρκετά απλή. Μόλις - πρέπει να οργανώσουμε την ομαλή και ασφαλή πρόσβαση στον κοινόχρηστο πόρο. Δύο - πρέπει να εκτελέσετε τα νήματα με κάποια σειρά.
Είναι θέμα υλοποίησης. Και εδώ βρισκόμαστε αντιμέτωποι με πολλές δυσκολίες, για τις οποίες μιλούν με προσμονή (και ίσως για καλό λόγο). Ας ξεκινήσουμε με τον κοινόχρηστο πόρο.

Κοινόχρηστος πόρος σε πολλαπλά νήματα

Προτείνω να δείξουμε αμέσως το πρόβλημα με ένα απλό παράδειγμα. Το καθήκον του είναι να εκτοξεύσει 200 ​​νήματα κλάσης CounterThread. Κάθε νήμα λαμβάνει μια αναφορά σε ένα μεμονωμένο αντικείμενο Μετρητής. Κατά την εκτέλεση, το νήμα καλεί μια μέθοδο σε αυτό το αντικείμενο αύξηση Μετρητήςχίλιες φορές. Μεταβλητή αυξήσεων μεθόδου μετρητήςαπό 1. Έχοντας ξεκινήσει 200 ​​νήματα, περιμένουμε να τελειώσουν (απλώς κοιμόμαστε για 1 δευτερόλεπτο - αυτό είναι αρκετό). Και στο τέλος εκτυπώνουμε το αποτέλεσμα. Κοιτάξτε τον κώδικα - κατά τη γνώμη μου, όλα είναι αρκετά διαφανή εκεί:

<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public void increaseCounter() { counter++; } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

δημόσια τάξη CounterTester

για (int i = 0; i< 200 ; i ++ ) {

ct. αρχή();

Νήμα. ύπνος (1000);

μετρητής τάξης

ιδιωτικός μακρύς μετρητής = 0L ;

δημόσιο κενό αύξησηCounter() (

counter++;

public long getCounter() (

μετρητής επιστροφής ;

ιδιωτικός μετρητής ;

Αυτό. μετρητής = μετρητής ;

@Καταπατώ

public void run() (

για (int i = 0; i< 1000 ; i ++ ) {

Λογικά, θα πρέπει να έχουμε το εξής αποτέλεσμα - 200 νήματα των 1000 προσθηκών = 200000. Αλλά, φρίκη των φρίκης, αυτό δεν ισχύει καθόλου. Τα αποτελέσματά μου ποικίλλουν, αλλά προφανώς όχι 200.000 Ποιο είναι το πρόβλημα; Το πρόβλημα είναι ότι προσπαθούμε να καλέσουμε μια μέθοδο από 200 νήματα ταυτόχρονα αύξηση Μετρητής. Με την πρώτη ματιά, τίποτα τρομερό δεν συμβαίνει σε αυτό - απλώς προσθέτουμε στη μεταβλητή μετρητήςμονάδα. Τι είναι τόσο τρομερό σε αυτό;
Το τρομερό είναι ότι ο φαινομενικά αβλαβής κώδικας για την προσθήκη ενός εκτελείται στην πραγματικότητα σε περισσότερα από ένα βήματα. Αρχικά, διαβάζουμε την τιμή της μεταβλητής στον καταχωρητή, προσθέτουμε μία σε αυτήν και μετά γράφουμε το αποτέλεσμα πίσω στη μεταβλητή. Όπως μπορείτε να δείτε, υπάρχουν περισσότερα από ένα βήματα (μυστικά, είναι ακόμη περισσότερα από τα τρία που περιέγραψα). Και τώρα φανταστείτε ότι δύο νήματα (ή ακόμα περισσότερα) διαβάζουν ταυτόχρονα την τιμή μιας μεταβλητής - για παράδειγμα, υπήρχε μια τιμή 99. Τώρα και τα δύο νήματα προσθέτουν ένα στο 99, και τα δύο παίρνουν 100 και τα δύο γράφουν αυτήν την τιμή στη μεταβλητή. Τι συμβαίνει εκεί; Είναι εύκολο να δεις ότι θα είναι 100. Αλλά θα έπρεπε να είναι 101. Μπορεί να είναι ακόμα χειρότερο αν κάποιο νήμα «κατάφερε» να μετρήσει το 98 και «κολλήσει» στην ουρά των νημάτων για εκτέλεση. Τότε δεν θα πάρουμε ούτε 100. Πρόβλημα :)

Η πρόσβαση σε κοινόχρηστους πόρους είναι ένα από τα μεγαλύτερα προβλήματα με το multithreading. Γιατί είναι πολύ ύπουλη. Μπορείτε να κάνετε τα πάντα πολύ αξιόπιστα, αλλά τότε η απόδοση θα πέσει. Και από τη στιγμή που θα δώσεις «χαλαρή» (συνειδητά, για χάρη της παραγωγικότητας), αναπόφευκτα θα προκύψει μια κατάσταση όπου η «χαλαρή» θα βγει σε όλο της το μεγαλείο.

Η μαγική λέξη συγχρονίζεται

Τι μπορεί να γίνει για να απαλλαγούμε από την κατάσταση που βρισκόμαστε με τις υπέροχες ροές μας. Ας σκεφτούμε λίγο πρώτα. Φτάνοντας στο μαγαζί πηγαίνουμε στο ταμείο για να πληρώσουμε. Το ταμείο εξυπηρετεί μόνο ένα άτομο κάθε φορά. Όλοι κάνουμε ουρά για αυτήν. Ουσιαστικά, η ταμειακή μηχανή γίνεται αποκλειστικός πόρος που μπορεί να χρησιμοποιηθεί μόνο από έναν πελάτη κάθε φορά. Το Multithreading προσφέρει ακριβώς την ίδια μέθοδο - μπορείτε να ορίσετε κάποιον πόρο ως αποκλειστικά που παρέχεται μόνο σε ένα νήμα τη φορά. Αυτός ο πόρος ονομάζεται "παρακολούθηση". Αυτό είναι το πιο κοινό αντικείμενο που πρέπει να «συλλάβει» ένα νήμα. Όλα τα νήματα που θέλουν να αποκτήσουν πρόσβαση σε αυτήν την οθόνη (αντικείμενο) βρίσκονται στην ουρά. Επιπλέον, δεν χρειάζεται να γράψετε ειδικό κωδικό για αυτό - απλώς προσπαθήστε να "αιχμαλωτίσετε" την οθόνη. Πώς όμως το ορίζουμε αυτό; Ας το καταλάβουμε.
Προτείνω να εκτελέσετε το παράδειγμά μας, αλλά με μια επιπλέον λέξη στην περιγραφή της μεθόδου αύξηση Μετρητής- αυτή η λέξη συγχρονισμένη.

πακέτο edu.javacourse.counter; δημόσια κλάση CounterTester ( public static void main(String args) ρίχνει InterruptedException ( Counter counter = new Counter(); for(int i=0; i<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public synchronized void increaseCounter() { counter++; } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

πακέτο edu. javacourse. μετρητής ;

δημόσια τάξη CounterTester

δημόσιο στατικό κενό main (String args) ρίχνει InterruptedException(

Μετρητής μετρητής = νέος μετρητής () ;

για (int i = 0; i< 200 ; i ++ ) {

CounterThread ct = νέο CounterThread(counter);

ct. αρχή();

Νήμα. ύπνος (1000);

Σύστημα. έξω . println ("Μετρητής:" + μετρητής . getCounter () );

μετρητής τάξης

ιδιωτικός μακρύς μετρητής = 0L ;

δημόσιο συγχρονισμένο κενό αύξησηCounter() (

counter++;

public long getCounter() (

μετρητής επιστροφής ;

η κλάση CounterThread επεκτείνει το νήμα

ιδιωτικός μετρητής ;

δημόσιο CounterThread (Counter counter) (

Αυτό. μετρητής = μετρητής ;

@Καταπατώ

public void run() (

για (int i = 0; i< 1000 ; i ++ ) {

μετρητής. αύξησηCounter();

Και... ιδού. Όλα λειτούργησαν. Παίρνουμε το αναμενόμενο αποτέλεσμα - 200000. Τι κάνει αυτή η μαγική λέξη - συγχρονισμένη ?
Λέξη συγχρονισμένηλέει ότι για να μπορέσει ένα νήμα να καλέσει αυτήν τη μέθοδο στο αντικείμενό μας, πρέπει να «συλλάβει» το αντικείμενό μας και στη συνέχεια να εκτελέσει την επιθυμητή μέθοδο. Για άλλη μια φορά και προσεκτικά (μερικές φορές προτείνεται μια ελαφρώς διαφορετική προσέγγιση, η οποία κατά τη γνώμη μου είναι εξαιρετικά επικίνδυνη και λανθασμένη - θα την περιγράψω λίγο αργότερα) - πρώτα το νήμα «συλλαμβάνει» (κλειδώνει - από τη λέξη κλειδαριά - κλειδαριά, μπλοκ) το αντικείμενο της οθόνης (στην περίπτωσή μας είναι αντικείμενο κλάσης Μετρητής) και μόνο μετά από αυτό το νήμα θα μπορεί να εκτελέσει τη μέθοδο αύξηση Μετρητής. Αποκλειστικά, εντελώς μόνος χωρίς ανταγωνιστές.
Υπάρχει και άλλη ερμηνεία συγχρονισμένη, το οποίο μπορεί να είναι παραπλανητικό - ακούγεται κάπως έτσι: σε μια συγχρονισμένη μέθοδο, πολλά νήματα δεν μπορούν να εισέλθουν ταυτόχρονα. Αυτό δεν είναι αληθινό. Γιατί τότε αποδεικνύεται ότι αν μια κλάση έχει πολλές μεθόδους συγχρονισμένη, τότε μπορείτε να εκτελέσετε ταυτόχρονα δύο διαφορετικές μεθόδους του ίδιου αντικειμένου, που επισημαίνονται ως συγχρονισμένη. Αυτό δεν είναι αληθινό. Εάν μια κλάση έχει 2, 3 ή περισσότερες μεθόδους συγχρονισμένη, τότε όταν εκτελεστεί τουλάχιστον ένα, μπλοκάρεται ολόκληρο το αντικείμενο. Αυτό σημαίνει ότι όλες οι μέθοδοι που ορίζονται ως συγχρονισμένηδεν είναι διαθέσιμο σε άλλα νήματα. Εάν η μέθοδος δεν ορίζεται ως τέτοια. τότε δεν είναι πρόβλημα - κάντε το για την υγεία σας.
Και για άλλη μια φορά - πρώτα «αιχμαλώτισαν», μετά εφάρμοσαν τη μέθοδο, μετά «απελευθέρωσαν». Τώρα το αντικείμενο είναι ελεύθερο και όποιος το απαθανάτισε πρώτος από τα ρέματα έχει δίκιο.
Εάν η μέθοδος δηλωθεί ως στατικός, τότε ολόκληρη η κλάση γίνεται αντικείμενο παρακολούθησης και η πρόσβαση σε αυτήν αποκλείεται στο επίπεδο όλων των αντικειμένων αυτής της κλάσης.

Συζητώντας το άρθρο, επισήμαναν μια ανακρίβεια που έκανα επίτηδες (για λόγους απλότητας), αλλά μάλλον έχει νόημα να το αναφέρω. Πρόκειται για τη μέθοδο getCounter. Αυστηρά μιλώντας, θα πρέπει επίσης να χαρακτηριστεί ως συγχρονισμένη, γιατί τη στιγμή που αλλάζει η μεταβλητή μας, κάποιο άλλο νήμα θα θέλει να τη διαβάσει. Και για να αποφευχθούν προβλήματα, η πρόσβαση σε αυτή τη μεταβλητή πρέπει να συγχρονίζεται με όλες τις μεθόδους.
Αν και ως προς getCounter, τότε εδώ μπορείτε να χρησιμοποιήσετε ένα ακόμα πιο ενδιαφέρον χαρακτηριστικό - την ατομικότητα των λειτουργιών. Μπορείτε να διαβάσετε σχετικά στο άρθρο της Atomic Access. Η βασική ιδέα είναι ότι η ανάγνωση και η γραφή ορισμένων στοιχειωδών τύπων και αναφορών γίνεται σε ένα βήμα και είναι, καταρχήν, ασφαλής. Αν το χωράφι μετρητήςήταν για παράδειγμα ενθ, τότε θα ήταν δυνατή η ανάγνωση όχι σε μια σύγχρονη μέθοδο. Για τον τύπο μακρύςΚαι διπλόπρέπει να δηλώσουμε μια μεταβλητή μετρητήςΠως πτητικός. Γιατί αυτό μπορεί να είναι ενδιαφέρον - πρέπει να το λάβουμε υπόψη ενθαποτελείται από 4 byte και μπορείτε να φανταστείτε την κατάσταση ότι ο αριθμός δεν θα γραφτεί σε ένα βήμα. Αλλά αυτό είναι καθαρά θεωρητικό - το JVM μας εγγυάται ότι η ανάγνωση και η εγγραφή του στοιχειώδους τύπου int γίνεται σε ένα βήμα και ούτε ένα νήμα δεν μπορεί να μπει σε αυτή τη λειτουργία και να καταστρέψει κάτι.

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

πακέτο edu.javacourse.counter; δημόσια κλάση CounterTester ( public static void main(String args) ρίχνει InterruptedException ( Counter counter = new Counter(); for(int i=0; i<200; i++) { CounterThread ct = new CounterThread(counter); ct.start(); } Thread.sleep(1000); System.out.println("Counter:" + counter.getCounter()); } } class Counter { private long counter = 0L; public void increaseCounter() { synchronized(this) { counter++; } } public long getCounter() { return counter; } } class CounterThread extends Thread { private Counter counter; public CounterThread(Counter counter) { this.counter = counter; } @Override public void run() { for(int i=0; i<1000; i++) { counter.increaseCounter(); } } }

πακέτο edu. javacourse. μετρητής ;

δημόσια τάξη CounterTester

δημόσιο στατικό κενό main (String args) ρίχνει InterruptedException(

Μετρητής μετρητής = νέος μετρητής () ;

για (int i = 0; i< 200 ; i ++ ) {