Τα threads στην Java αναφέρονται σε διεργασίες εκτέλεσης που λειτουργούν παράλληλα μεταξύ τους. Ένα thread αντιπροσωπεύει έναν ανεξάρτητο ροή εκτέλεσης εντολών εντός ενός προγράμματος. Η παράλληλη εκτέλεση threads μπορεί να βελτιώσει την απόδοση και την αποκρισιμότητα εφαρμογών, επιτρέποντας την ταυτόχρονη εκτέλεση πολλαπλών εργασιών.
Για να χρησιμοποιήσουμε threads στην Java, μπορούμε να δημιουργήσουμε μια κλάση που υλοποιεί τη διεπαφή Runnable
και να υλοποιήσουμε τη μέθοδο run()
. Έπειτα, μπορούμε να δημιουργήσουμε ένα αντικείμενο Thread
, περνώντας το αντικείμενο της κλάσης που υλοποιεί το Runnable
ως παράμετρο. Όταν καλέσουμε τη μέθοδο start()
του αντικειμένου Thread
, το thread θα ξεκινήσει την εκτέλεση της μεθόδου run()
.
Τα threads μπορούν να χρησιμοποιηθούν για να επιτελέσουν παράλληλες εργασίες, όπως την επεξεργασία δεδομένων, τη διαχείριση εκδηλώσεων ή την αλληλεπίδραση με τον χρήστη. Η Java παρέχει προηγμένες δομές δεδομένων και μέθοδους για τον συγχρονισμό και την επικοινωνία μεταξύ των threads, προκειμένου να αποφευχθούν πιθανά προβλήματα ασφάλειας και συγκρούσεων.
Η χρήση threads στην Java μπορεί να βοηθήσει στην ανάπτυξη πολυνηματικών εφαρμογών, που μπορούν να εκτελούν πολλαπλές εργασίες ταυτόχρονα και να επωφελούνται από τους πόρους του συστήματος αποτελεσματικά.
Υπάρχουν δύο τρόποι για να δημιουργήσετε ένα Thread στη Java:
Μπορεί να δημιουργηθεί διαδόχως της κλάσης Thread και η run() μέθοδος της να υπερκαλυφθεί (override):
public class MyThread extends Thread { public void run() { // κώδικας που θα εκτελεστεί στο Thread } }
Ή μπορείτε να δημιουργήσετε μια κλάση που υλοποιεί τη διεπαφή Runnable και να υλοποιήσετε τη μέθοδο run():
public class MyRunnable implements Runnable { public void run() { // κώδικας που θα εκτελεστεί στο Thread } }
Στη συνέχεια, μπορείτε να δημιουργήσετε ένα αντικείμενο της κλάσης MyThread ή της κλάσης MyRunnable και να καλέσετε την μέθοδο start() για να ξεκινήσει η εκτέλεση του Thread:
// Δημιουργία ενός νέου νήματος MyThread και εκκίνηση του MyThread thread1 = new MyThread(); thread1.start(); // Δημιουργία ενός αντικειμένου MyRunnable MyRunnable runnable = new MyRunnable(); // Δημιουργία ενός νέου νήματος Thread με το αντικείμενο MyRunnable και εκκίνηση του Thread thread2 = new Thread(runnable); thread2.start();
Ο παραπάνω κώδικας δημιουργεί και εκκινεί δύο νήματα εκτέλεσης σε μια Java εφαρμογή. Πιο συγκεκριμένα:
- Γραμμή 1: Δημιουργείται ένα νέο αντικείμενο τύπου
MyThread
, το οποίο πιθανότατα είναι μια προσαρμοσμένη κλάση που επεκτείνει τηνThread
. - Γραμμή 2: Το νήμα
thread1
ξεκινά την εκτέλεσή του με την κλήση της μεθόδουstart()
. Αυτό εκτελεί τον κώδικα που έχει υλοποιηθεί στηνMyThread
. - Γραμμή 5: Δημιουργείται ένα αντικείμενο τύπου
MyRunnable
, το οποίο πιθανότατα υλοποιεί τη διεπαφήRunnable
. - Γραμμή 8: Δημιουργείται ένα νέο νήμα τύπου
Thread
με το αντικείμενοrunnable
ως παράμετρο. - Γραμμή 9: Το νήμα
thread2
ξεκινά την εκτέλεσή του με την κλήση της μεθόδουstart()
. Αυτό εκτελεί τον κώδικα που έχει υλοποιηθεί στηνMyRunnable
.
Ο κώδικας δημιουργεί δύο νήματα εκτέλεσης που μπορούν να εκτελούνται παράλληλα. Ο τρόπος εκτέλεσης του κώδικα σε κάθε νήμα εξαρτάται από την υλοποίηση των κλάσεων MyThread
και MyRunnable
.
Σημείωση: Η δημιουργία ενός νέου Thread μπορεί να είναι δαπανηρή σε ό,τι αφορά τους πόρους του συστήματος, και επομένως θα πρέπει να χρησιμοποιείτε με προσοχή όταν δημιουργείτε πολλαπλά Threads.
[adinserter block=”2″]
Αν η κλάση επεκτείνει την κλάση Thread, τότε το Thread μπορεί να εκτελεστεί δημιουργώντας μια νέα έκδοση της κλάσης και καλώντας τη μέθοδο start() της κλάσης:
public class MyThread extends Thread { public void run() { // κώδικας που θα εκτελεστεί στο Thread } } MyThread thread = new MyThread(); thread.start(); // Ξεκινά την εκτέλεση του Thread
Ο παραπάνω κώδικας δημιουργεί ένα νέο νήμα εκτέλεσης (thread) με τη χρήση της κλάσης MyThread
που κληρονομεί από την κλάση Thread
. Η λειτουργία του νήματος ορίζεται μέσα στη μέθοδο run()
, όπου μπορείτε να τοποθετήσετε τον κώδικα που θέλετε να εκτελεστεί στο νήμα.
Στον παραπάνω κώδικα, η μέθοδος run()
δεν έχει καταχωρηθεί κάποιος συγκεκριμένος κώδικας. Πρέπει να προσθέσετε τις ενέργειες που επιθυμείτε να εκτελούνται μέσα στη μέθοδο run()
. Μόλις το νήμα ξεκινήσει με την κλήση της μεθόδου start()
, ο κώδικας που έχει τοποθετηθεί μέσα στη μέθοδο run()
θα εκτελεστεί σε ένα ξεχωριστό νήμα εκτέλεσης.
Αν η κλάση υλοποιεί τη διεπαφή Runnable, τότε μπορεί να εκτελεστεί δημιουργώντας ένα αντικείμενο της κλάσης και δημιουργώντας ένα αντικείμενο Thread για να το εκτελέσει:
public class MyRunnable implements Runnable { public void run() { // Κώδικας που θα εκτελεστεί στο Thread // Εδώ μπορείτε να τοποθετήσετε τις ενέργειες που θέλετε να εκτελεστούν σε ένα ξεχωριστό Thread } } MyRunnable runnable = new MyRunnable(); // Δημιουργία ενός αντικειμένου της κλάσης MyRunnable Thread thread = new Thread(runnable); // Δημιουργία ενός αντικειμένου Thread με το MyRunnable αντικείμενο ως όρισμα thread.start(); // Έναρξη του Thread και εκτέλεση του κώδικα που βρίσκεται μέσα στη μέθοδο run()
Ο παραπάνω κώδικας δημιουργεί ένα νέο Thread και τον εκκινεί για να εκτελέσει κάποιον κώδικα στο παρασκήνιο (background). Ακολουθούν οι λεπτομέρειες:
- Δημιουργείται η κλάση
MyRunnable
, η οποία υλοποιεί το αναγκαίο interfaceRunnable
. Αυτό σημαίνει ότι η κλάση περιέχει τη μέθοδοrun()
που θα εκτελεστεί από το Thread. - Στο εσωτερικό της κλάσης
MyRunnable
τοποθετείται ο κώδικας που θα εκτελεστεί όταν το Thread ξεκινήσει. Αυτός ο κώδικας πρέπει να βρίσκεται μέσα στη μέθοδοrun()
. - Δημιουργείται ένα αντικείμενο
MyRunnable
με το όνομαrunnable
. - Δημιουργείται ένα νέο αντικείμενο
Thread
με όρισμα τοrunnable
, που είναι η υλοποίηση τουRunnable
. - Τέλος, καλείται η μέθοδος
start()
στο αντικείμενοThread
, η οποία ξεκινά το Thread και εκτελεί τον κώδικα που βρίσκεται στη μέθοδοrun()
της κλάσηςMyRunnable
.
Ο κώδικας αυτός επιτρέπει την εκτέλεση εργασιών σε διαφορετικό Thread από αυτόν που εκτελείται η κύρια ροή εκτέλεσης (main Thread). Αυτό μπορεί να είναι χρήσιμο όταν θέλουμε να εκτελέσουμε μια διαδικασ
ία παράλληλα με την κύρια εκτέλεση του προγράμματος.
Αν θέλουμε να εκτελέσουμε πολλαπλές εργασίες ταυτόχρονα, μπορούμε να δημιουργήσουμε πολλά νήματα (Threads). Αυτά τα νήματα μπορούν να εκτελούνται παράλληλα, βελτιώνοντας την απόδοση του προγράμματος.
Για να επιτευχθεί αυτό, μπορούμε να δημιουργήσουμε μια κλάση που υλοποιεί τη διεπαφή Runnable. Αυτή η κλάση πρέπει να υλοποιεί τη μέθοδο run(), όπου ο κώδικας της εργασίας που θέλουμε να εκτελεστεί παράλληλα θα τοποθετηθεί.
Έπειτα, μπορούμε να δημιουργήσουμε ένα αντικείμενο της κλάσης μας και να το περάσουμε στον κατασκευαστή ενός αντικειμένου Thread. Τέλος, καλούμε τη μέθοδο start() στο αντικείμενο Thread για να ξεκινήσει η εκτέλεση του κώδικα που βρίσκεται στη μέθοδο run() της κλάσης Runnable.
public class MyRunnable implements Runnable { public void run() { // Κώδικας που θα εκτελεστεί στο Thread } } // Δημιουργία ενός αντικειμένου MyRunnable MyRunnable runnable = new MyRunnable(); // Δημιουργία ενός Thread και παροχή του MyRunnable ως παραμέτρου Thread thread = new Thread(runnable); // Έναρξη του Thread για να εκτελεστεί ο κώδικας του MyRunnable thread.start();
Ο παραπάνω κώδικας δημιουργεί ένα νέο νήμα εκτέλεσης (thread) και το συσχετίζει με ένα αντικείμενο τύπου MyRunnable
, το οποίο υλοποιεί τη διεπαφή Runnable
.
Ο κώδικας που θα εκτελεστεί βρίσκεται μέσα στη μέθοδο run()
της κλάσης MyRunnable
. Όταν το νήμα εκτελείται, η μέθοδος run()
εκτελείται στο παράλληλο νήμα, εκτελώντας τον κώδικα που έχει τοποθετηθεί μέσα σε αυτήν.
Η δημιουργία του νήματος γίνεται με τη δημιουργία ενός αντικειμένου Thread
και την παροχή του MyRunnable
ως παραμέτρου στον κατασκευαστή του Thread
. Στη συνέχεια, καλείται η μέθοδος start()
του νήματος για να ξεκινήσει η εκτέλεση του κώδικα στο run()
.
Συνολικά, ο παραπάνω κώδικας δημιουργεί ένα νέο νήμα εκτέλεσης και εκτελεί τον κώδικα που έχει τοποθετηθεί μέσα στη μέθοδο run()
του αντικειμένου MyRunnable
σε ένα παράλληλο νήμα εκτέλεσης.
Η χρήση της διεπαφής Runnable
είναι προτιμότερη από την επέκταση της κλάσης Thread
για την υλοποίηση πολυνηματικού κώδικα. Αυτό συμβαίνει επειδή η χρήση της διεπαφής Runnable
επιτρέπει την ευελιξία στην κληρονομική σχέση με άλλες κλάσεις και την επαναχρησιμοποίηση του ίδιου αντικειμένου σε πολλαπλά νήματα εργασίας.
Καθώς τα νήματα εκτελούνται παράλληλα με άλλα τμήματα του προγράμματος, δεν μπορούμε να προβλέψουμε τη σειρά με την οποία θα εκτελεστεί ο κώδικας. Όταν τα νήματα και το κύριο πρόγραμμα διαβάζουν και εγγράφουν στις ίδιες μεταβλητές, οι τιμές των μεταβλητών μπορεί να είναι απρόβλεπτες. Αυτά τα προβλήματα ονομάζονται προβλήματα συγχρονισμού (concurrency problems).
Οι συνήθεις προβληματικές καταστάσεις που αντιμετωπίζουμε στον συγχρονισμό των νημάτων περιλαμβάνουν:
- Συνθήκες ανταγωνισμού (Race conditions): Όταν δύο ή περισσότερα νήματα προσπαθούν να ανανεώσουν μια κοινόχρηστη μεταβλητή ταυτόχρονα, μπορεί να προκύψει μη προβλέψιμη συμπεριφορά. Αυτό συμβαίνει όταν ο χρόνος εκτέλεσης των νημάτων δεν μπορεί να προβλεφθεί, και οι ενέργειες των νημάτων εμπλέκονται μεταξύ τους.
- Αδιέξοδες καταστάσεις (Deadlocks): Όταν δύο ή περισσότερα νήματα περιμένουν η ένα άλλο νήμα να ολοκληρώσει την εκτέλεσή του και κανένα από αυτά τα νήματα δεν μπορεί να προχωρήσει, τότε βρισκόμαστε σε αδιέξοδη κατάσταση. Τα νήματα είναι αποκλεισμένα ο ένας από τον άλλο, αναμένοντας ακαθόριστα γεγονότα που δεν θα συμβούν ποτέ.
- Αποκλεισμός (Starvation): Όταν ένα νήμα έχει προτεραιότητα στην εκτέλεση και τα υπόλοιπα νήματα περιμένουν για πολύ χρόνο, δεν μπορούν να προχωρή
σουν και δεν μπορούν να εκτελέσουν τις εργασίες τους, τότε λέμε ότι τα νήματα βρίσκονται σε κατάσταση αποκλεισμού. Ένα νήμα μπορεί να “πεινάει” από πόρους ή προτεραιότητες και να μην μπορεί να εκτελεστεί ποτέ.
Για να αποφύγουμε τα προβλήματα συγχρονισμού που μπορεί να προκύψουν κατά την παράλληλη εκτέλεση νημάτων, είναι σημαντικό να χρησιμοποιούμε μηχανισμούς συγχρονισμού. Μια από τις προσεγγίσεις που μπορούμε να ακολουθήσουμε είναι η χρήση συγχρονισμένων δομών δεδομένων, όπως synchronized blocks και locks, προκειμένου να διασφαλίσουμε ότι οι μεταβλητές και τα αντικείμενα είναι προσβάσιμα με ασφαλή τρόπο από τα νήματα.
Με τη χρήση synchronized blocks, μπορούμε να επιτύχουμε τον συγχρονισμό της πρόσβασης σε κοινόχρηστους πόρους μεταξύ των νημάτων. Χρησιμοποιώντας κατάλληλα κλειδιά συγχρονισμού, μπορούμε να εξασφαλίσουμε ότι μόνο ένα νήμα μπορεί να έχει πρόσβαση σε ένα συγκεκριμένο τμήμα κώδικα κάθε φορά.
Επιπλέον, είναι σημαντικό να επιλέξουμε την κατάλληλη συγχρονισμένη δομή δεδομένων για το συγκεκριμένο πρόβλημα συγχρονισμού που αντιμετωπίζουμε. Για παράδειγμα, μπορούμε να χρησιμοποιήσουμε την κλάση Collections.synchronizedMap()
για να δημιουργήσουμε μια συγχρονισμένη Map
που είναι ασφαλής για παράλληλη πρόσβαση από διάφορα νήματα.
Συνολικά, η χρήση συγχρονισμένων δομών δεδομένων και η επιλογή της κατάλληλης στρατηγικής συγχρονισμού είναι σημαντικές πρακτικές για να αποφύγουμε προβλήματα συγχρονισμού και να εξασφαλίσουμε την ορθή λειτουργία των νημάτων.
[adinserter block=”3″]
Για παράδειγμα, η χρήση synchronized blocks στην Java μας επιτρέπει να διασφαλίσουμε ότι μόνο ένα νήμα μπορεί να εκτελέσει τμήμα κώδικα που έχει τοποθετηθεί μέσα σε αυτό το block κάθε φορά. Αντίστοιχα, μπορούμε να χρησιμοποιήσουμε locks για να ελέγξουμε την πρόσβαση σε αντικείμενα.
Είναι σημαντικό να έχουμε υπόψη ότι η χρήση συγχρονισμένων δομών δεδομένων μπορεί να επηρεάσει την απόδοση του προγράμματος, καθώς μπορεί να προκαλέσει καθυστερήσεις και κλειδώματα. Για αυτό το λόγο, πρέπει να χρησιμοποιούμε τα συγχρονισμένα blocks και τα locks με προσοχή και να προσπαθούμε να ελαχιστοποιούμε τον αριθμό των μεταβλητών και των αντικειμένων που χρησιμοποιούνται ταυτόχρονα.
Στην Java, υπάρχουν επίσης και άλλες δομές δεδομένων που μπορούν να χρησιμοποιηθούν για να αποφευχθούν προβλήματα συγχρονισμού, όπως οι ConcurrentHashMap και οι AtomicInteger. Αυτές οι δομές δεδομένων έχουν σχεδιαστεί έτσι ώστε να επιτρέπουν την παράλληλη πρόσβαση από πολλά νήματα στα δεδομένα, χωρίς να προκαλούν τα προβλήματα συγχρονισμού που αναφέρθηκαν προηγουμένως.
Τέλος, μπορούμε να χρησιμοποιήσουμε τα πρότυπα σχεδιασμού πολυνηματικότητας, όπως ο διαμοιρασμός εργασίας (work-sharing) και ο διαχωρισμός εργασίας (work-dividing), για να μειώσουμε τα προβλήματα συγχρονισμού και να βελτιστοποιήσουμε την απόδοση των νημάτων. Στο πρότυπο διαμοιρασμού εργασίας, κάθε νήμα αναλαμβάνει μια μικρότερη εργασία, ενώ στο πρότυπο διαχωρισμού εργασίας, η εργασία διαιρείται σε μικρότερα τμήματα και κάθε νήμα αναλαμβάνει ένα από αυτά τα τμήματα.
Παραδείγματα προτύπων σχεδιασμού πολυνηματικότητας είναι η αρχιτεκτονική Master-Worker και η αρχιτεκτονική MapReduce. Στην αρχιτεκτονική Master-Worker, ένας κύριος νήματος (master) αναλαμβάνει την εργασία διαμοιράζοντάς την σε πολλαπλά εργασιακά νήματα (workers). Κάθε worker νήμα εκτελεί το δικό του κομμάτι εργασίας ανεξάρτητα από τα υπόλοιπα. Στην αρχιτεκτονική MapReduce, τα δεδομένα διαιρούνται σε μικρότερα κομμάτια (map) και κάθε κομμάτι επεξεργάζεται ανεξάρτητα από τα υπόλοιπα κομμάτια. Στη συνέχεια, τα αποτελέσματα συνολικά συνδέονται για την τελική επεξεργασία (reduce).
Συνολικά, η παραλληλοποίηση είναι ένα ισχυρό εργαλείο για τη βελτίωση της απόδοσης των προγραμμάτων. Ωστόσο, είναι σημαντικό να λαμβάνονται υπόψη οι πιθανές συγκρούσεις και προβλήματα συγχρονισμού κατά την ταυτόχρονη εκτέλεση πολλαπλών νημάτων στον ίδιο κομμάτι κώδικα, καθώς επίσης και η υλοποίηση προτύπων σχεδιασμού που μειώνουν την πιθανότητα συγκρούσεων. Επιπλέον, η χρήση εργαλείων και βιβλιοθηκών που υποστηρίζουν την παραλληλοποίηση μπορεί να βοηθήσει στην ευκολία υλοποίησης της παραλληλοποίησης και στην αποφυγή προβλημάτων συγχρονισμού.
Τέλος, είναι σημαντικό να σημειωθεί ότι η παραλληλοποίηση δεν είναι πάντα η καλύτερη επιλογή για τη βελτίωση της απόδοσης του προγράμματος. Υπάρχουν περιπτώσεις όπου η υλοποίηση ενός αλγορίθμου με μόνο ένα νήμα μπορεί να είναι πιο αποδοτική από την υλοποίησή του με πολλαπλά νήματα.
Ένα παράδειγμα κώδικα όπου η τιμή της μεταβλητής amount είναι απρόβλεπτη εξαιτίας της πολυνηματικότητας είναι ο ακόλουθος:
public class Account { private int amount = 0; public void deposit(int value) { amount += value; } public void withdraw(int value) { amount -= value; } public int getAmount() { return amount; } } public class Main { public static void main(String[] args) { // Δημιουργία ενός αντικειμένου Account Account account = new Account(); // Δημιουργία και εκκίνηση του πρώτου νήματος (t1) Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { // Κατάθεση 1 μονάδας στον λογαριασμό account.deposit(1); } }); // Δημιουργία και εκκίνηση του δεύτερου νήματος (t2) Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { // Ανάληψη 1 μονάδας από τον λογαριασμό account.withdraw(1); } }); // Εκκίνηση των νημάτων t1 και t2 t1.start(); t2.start(); try { // Αναμονή μέχρι να ολοκληρωθούν τα νήματα t1 και t2 t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // Εκτύπωση του υπολοίπου του λογαριασμού System.out.println(account.getAmount()); } }
[adinserter block=”4″]
Ο παραπάνω κώδικας δημιουργεί έναν απλό λογαριασμό (Account) και τον χρησιμοποιεί από δύο νήματα (t1 και t2) για να πραγματοποιήσει καταθέσεις (deposit) και αναλήψεις (withdraw) χρημάτων. Οι καταθέσεις και αναλήψεις γίνονται 1000 φορές από κάθε νήμα.
Ο λογαριασμός έχει αρχική τιμή 0 και κάθε κατάθεση αυξάνει το ποσό κατά 1, ενώ κάθε ανάληψη μειώνει το ποσό κατά 1. Οι δύο νήματα εκτελούνται παράλληλα και προσπαθούν να τροποποιήσουν το ποσό του λογαριασμού ταυτόχρονα.
Για να αποφευχθούν ανεπιθύμητες καταστάσεις ανταγωνισμού (race conditions), η κατάθεση και η ανάληψη των χρημάτων προστατεύονται από τη λέξη-κλειδί synchronized
στις αντίστοιχες μεθόδους του αντικειμένου Account
.
Τέλος, μετά την ολοκλήρωση των νημάτων, εκτυπώνεται το τελικό ποσό του λογαριασμού.
Το παρακάτω παράδειγμα χρησιμοποιεί ένα κλείδωμα για να συγχρονίσει την πρόσβαση στη μεταβλητή amount:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Account { private int amount = 0; private Lock lock = new ReentrantLock(); public void deposit(int value) { lock.lock(); // Αποκτάμε τον κλείδωμα για να προστατεύσουμε την κοινόχρηστη μεταβλητή try { amount += value; // Προσθέτουμε την τιμή στο ποσό } finally { lock.unlock(); // Απελευθερώνουμε τον κλείδωμα } } public void withdraw(int value) { lock.lock(); // Αποκτάμε τον κλείδωμα για να προστατεύσουμε την κοινόχρηστη μεταβλητή try { amount -= value; // Αφαιρούμε την τιμή από το ποσό } finally { lock.unlock(); // Απελευθερώνουμε τον κλείδωμα } } public int getAmount() { lock.lock(); // Αποκτάμε τον κλείδωμα για να προστατεύσουμε την κοινόχρηστη μεταβλητή try { return amount; // Επιστρέφουμε το ποσό } finally { lock.unlock(); // Απελευθερώνουμε τον κλείδωμα } } } public class Main { public static void main(String[] args) { Account account = new Account(); // Δημιουργούμε ένα νέο λογαριασμό Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { account.deposit(1); // Καταθέτουμε 1 μονάδα στον λογαριασμό } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { account.withdraw(1); // Αναλαμβάνουμε ανάληψη 1 μονάδας από τον λογαριασμό } }); t1.start(); // Ξεκινάμε το νήμα t1 t2.start(); // Ξεκινάμε το νήμα t2 try { t1.join(); // Περιμένουμε το νήμα t1 να ολοκληρωθεί t2.join(); // Περιμένουμε το νήμα t2 να ολοκληρωθεί } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(account.getAmount()); // Εκτυπώνουμε το ποσό του λογαριασμού } }
Ο παραπάνω κώδικας υλοποιεί έναν απλό μηχανισμό λογαριασμού τραπεζικής κατάθεσης/ανάληψης με πολλαπλά νήματα. Ας δούμε τι κάνει ο κώδικας αναλυτικά:
- Ορίζεται η κλάση
Account
, η οποία διαχειρίζεται ένα ποσό (amount
) και ένα κλείδωμα (lock
) για την ασφαλή πρόσβαση στο ποσό. - Η κλάση
Account
περιλαμβάνει μεθόδους για την κατάθεση (deposit
) και την ανάληψη (withdraw
) ποσών από τον λογαριασμό, καθώς και μια μέθοδο για την ανάκτηση του τρέχοντος ποσού (getAmount
). - Στην κλάση
Main
δημιουργείται ένα αντικείμενο της κλάσηςAccount
. - Δημιουργούνται δύο νήματα (
t1
καιt2
) που χρησιμοποιούν λειτουργίες του λογαριασμού (deposit
καιwithdraw
) για επαναλαμβανόμενες καταθέσεις και αναλήψεις. - Τα νήματα εκκινούν (
start
) για να ξεκινήσουν τις εργασίες τους παράλληλα. - Το κυρίως νήμα (
main
) περιμένει (join
) τα νήματαt1
καιt2
να ολοκληρωθούν πριν συνεχίσει την εκτέλεσή του. - Εκτυπώνεται το τελικό ποσό του λογαριασμού με την χρήση της μεθόδου
getAmount
.
Συνολικά, ο κώδικας πραγματοποιεί 1000 καταθέσεις και 1000 αναλήψεις αξίας 1, ταυτόχρονα από δύο διαφορετικά νήματα. Το τελικό ποσό που εκτυπώνεται αντιπροσωπεύει το αποτέλεσμα των παράλληλων λειτουργιών κατάθεσης και ανάληψης.
Στο παρακάτω παράδειγμα, δημιουργούμε δύο νήματα τα οποία αυξάνουν κατά ένα μια μεταβλητή counter. Χρησιμοποιούμε την isAlive() μέθοδο για να βεβαιωθούμε ότι το ένα νήμα έχει ολοκληρωθεί προτού το άλλο ξεκινήσει να εκτελείται.
class CounterThread extends Thread { private int counter; // Η μέθοδος run εκτελείται όταν ξεκινά ο νήματος public void run() { for (int i = 0; i < 5; i++) { counter++; System.out.println("CounterThread: " + counter); } } // Ελέγχει αν το νήμα CounterThread έχει τελειώσει την εκτέλεσή του public boolean isCounterThreadFinished() { return !this.isAlive(); } } public class Main { public static void main(String[] args) { CounterThread thread1 = new CounterThread(); CounterThread thread2 = new CounterThread(); // Ξεκινά το πρώτο νήμα CounterThread thread1.start(); // Αναμονή μέχρι το πρώτο νήμα CounterThread να ολοκληρωθεί while (!thread1.isCounterThreadFinished()) {} // Ξεκινά το δεύτερο νήμα CounterThread thread2.start(); } }
Ο παραπάνω κώδικας υλοποιεί ένα παράδειγμα πολλαπλών νημάτων (multithreading) στην Java.
Αρχικά, ορίζεται η κλάση CounterThread
, η οποία επεκτείνει την κλάση Thread
. Μέσα σε αυτή την κλάση υπάρχει ένα ιδιωτικό πεδίο counter
τύπου int
, το οποίο αναπαριστά έναν μετρητή. Στη μέθοδο run
, η οποία είναι υποκατάστατη της μεθόδου run
της κλάσης Thread
, ο μετρητής αυξάνεται κατά ένα και εμφανίζεται στην οθόνη. Η μέθοδος isCounterThreadFinished
επιστρέφει true
αν το νήμα CounterThread
έχει ολοκληρώσει την εκτέλεσή του, δηλαδή αν δεν είναι ενεργό.
Στην κλάση Main
, στην μέθοδο main
, δημιουργούνται δύο αντικείμενα της κλάσης CounterThread
με τα ονόματα thread1
και thread2
. Αρχικά, ξεκινά το πρώτο νήμα thread1
με την κλήση της μεθόδου start
. Έπειτα, χρησιμοποιείται μια επανάληψη while
για να αναμείνει η εκτέλεση του νήματος thread1
να ολοκληρωθεί, ελέγχοντας την κατάσταση του με την κλήση της μεθόδου isCounterThreadFinished
. Όταν το πρώτο νήμα ολοκληρωθεί, ξεκινά το δεύτερο νήμα thread2
με την κλήση της μεθόδου start
.
Συνολικά, ο κώδικας εκτυπώνει τιμές μετρητή στην οθόνη από τα δύο νήματα thread1
και thread2
. Επειδή τα νήματα εκτελούνται παράλληλα, οι εκτυπώσεις μπορεί να εμφανιστούν σε οποιαδήποτε σειρά, ανάλογα με το πώς η εκτέλεση των νημάτων προωθείται από το σύστημα.