13. Νήματα. 13.1 Νήματα και χρήση νημάτων στη Java. 13.1.1. Τι είναι τα νήματα; 13.1.2. Τρία μέρη ενός νήματος



Σχετικά έγγραφα
Ορισµός Νήµα (thread) είναι µια ακολουθιακή ροή ελέγχου (δηλ. κάτι που έχει αρχή, ακολουθία εντολών και τέλος) σ ένα

Νήµαταστην Java. Συγχρονισµός νηµάτων Επικοινωνία νηµάτων Εκτελέσιµα αντικείµενα Νήµατα δαίµονες Οµάδες νηµάτων. Κατανεµηµένα Συστήµατα 11-1

Οντοκεντρικός Προγραμματισμός

Τ.Ε.Ι. Μεσολογγίου, Τµήµα τηλεπικοινωνιακών Συστημάτων & Δικτύων

Το παρακάτω πρόγραμμα ορίζει δυο κλάσεις την SimpleThread και την TwoThreadsTest:

Καρακασίδης Αλέξανδρος Καστίδου Γεωργία Παπαφώτη Μαρία Πέτσιος Κων/νος Στέφανος Σαλτέας Καλογεράς Παναγιώτης. Threads in Java ΝΗΜΑΤΑ ΣΤΗ JAVA

Διάλεξη Εισαγωγή στη Java, Μέρος Γ

Προγραμματισμός ΙΙ (Java) 10. Πολυνηματικές εφαρμογές

Κατανεμημένα Συστήματα

Κατανεμημένα Συστήματα: Θεωρία και Προγραμματισμός. Ενότητα # 8: Ταυτοχρονισμός και νήματα Διδάσκων: Γεώργιος Ξυλωμένος Τμήμα: Πληροφορικής

Αντικειμενοστρεφής Προγραμματισμός

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Αντικείμενα με πίνακες. Constructors. Υλοποίηση Στοίβας

2.1 Αντικειµενοστρεφής προγραµµατισµός

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Αναφορές

6. Εξαιρέσεις στη γλώσσα Java

Κινητά και Διάχυτα Συστήματα. Ενότητα # 3: Νήματα και ταυτοχρονισμός Διδάσκων: Γεώργιος Ξυλωμένος Τμήμα: Πληροφορικής

Το πρόγραμμα HelloWorld.java. HelloWorld. Κλάσεις και Αντικείμενα (2) Ορισμός μιας Κλάσης (1) Παύλος Εφραιμίδης pefraimi <at> ee.duth.

Κλάσεις και Αντικείµενα

Αντικειµενοστρεφής Προγραµµατισµός

2 Ορισμός Κλάσεων. Παράδειγμα: Μηχανή για Εισιτήρια. Δομή μιας Κλάσης. Ο Σκελετός της Κλάσης για τη Μηχανή. Ορισμός Πεδίων 4/3/2008

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Constructors, equals, tostring

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Αναφορές Στοίβα και Σωρός Αναφορές-Παράμετροι

Ειδικά Θέματα Προγραμματισμού

Εργαστήριο 1-1 η Άσκηση - Ανάλυση

Εισαγωγή στην Java. Module 9: Threads. Prepared by Chris Panayiotou for EPL /03/2004

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα

Αντικειμενοστρεφής Προγραμματισμός Διάλεξη 9 : ΕΞΑΙΡΕΣΕΙΣ ΚΑΙ Ο ΧΕΙΡΙΣΜΟΣ ΤΟΥΣ

Γενικά (για τις γραπτές εξετάσεις)

Dr. Garmpis Aristogiannis - EPDO TEI Messolonghi

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Αναφορές Στοίβα και Σωρός Μνήμης Αντικείμενα ως ορίσματα

1. Ξεκινώντας. 1.1 Τι είναι η Java. PDF created with FinePrint pdffactory Pro trial version

Κλάσεις στη Java. Παύλος Εφραιμίδης. Java Κλάσεις στη Java 1

Κεφάλαιο 3. Διδακτικοί Στόχοι

Κλάσεις στη Java. Στοίβα - Stack. Δήλωση της κλάσης. ΗκλάσηVector της Java. Ηκλάση Stack

Οι δομές δεδομένων στοίβα και ουρά

ΕΡΓΑΣΤΗΡΙΟ 16. Χρησιμοποιώντας τον Αποσφαλματιστή (Debugger) του Eclipse

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Μαθήματα από τα εργαστήρια

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Αντικείμενα με πίνακες. Constructors. Υλοποίηση Στοίβας

Κατακερματισμός (Hashing)

3 Αλληλεπίδραση Αντικειμένων

14. Δικτύωση με Java Δικτύωση με Java Sockets Δημιουργία της σύνδεσης Διευθυνσιοδότηση της σύνδεσης

Συλλογές, Στοίβες και Ουρές

Προγραμματισμός ΙI (Θ)

HelloWorld. Παύλος Εφραιμίδης. Java Το πρόγραμμα HelloWorld 1

Προγραμματισμός Ι. Δυναμική Διαχείριση Μνήμης. Δημήτρης Μιχαήλ. Ακ. Έτος Τμήμα Πληροφορικής και Τηλεματικής Χαροκόπειο Πανεπιστήμιο

Κρίσιμη Περιοχή Υπό Συνθήκη (Conditional Critical Regions) Ταυτόχρονος Προγραμματισμός 1

Κεφάλαιο 1. Νήματα (Threads). Time Sharing

Περιεχόµενα. 1 Εισαγωγή στις οµές εδοµένων 3. 2 Στοίβα (Stack) 5

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Αναφορές Αντικείμενα ως ορίσματα

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Δημιουργώντας δικές μας Κλάσεις και Αντικείμενα

Λύβας Χρήστος Αρχική επιµέλεια Πιτροπάκης Νικόλαος και Υφαντόπουλος Νικόλαος

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Μέθοδοι

ΠΛΗΡΟΦΟΡΙΚΗ Ι JAVA Τμήμα θεωρίας με Α.Μ. σε 3, 7, 8 & 9 6/12/07

Δομημένος Προγραμματισμός

Διεργασίες (μοντέλο μνήμης & εκτέλεσης) Προγραμματισμός II 1

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Εισαγωγή στη Java

Πρόγραµµα 9.1 Πέρασµα δεδοµένων στην µνήµη

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Εισαγωγή στη Java II

ΠΛΗΡΟΦΟΡΙΚΗ ΙΙ (JAVA) 11/3/2008

Ελεγκτές/Παρακολουθητές (Monitors) Ταυτόχρονος Προγραμματισμός 1

Κεφάλαιο 4 Διεργασίες Β Τάξη ΕΠΑΛ

Αντικειμενοστραφής Προγραμματισμός I(5 ο εξ) Εργαστήριο #4 ο : Αποσφαλμάτωση (debugging), μετατροπές

12.6. Άσκηση 6 - [αξιοποίηση γραφικής διεπαφής (GUI)] (έκδοση 2006)

Εισαγωγή στην Πληροφορική

ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ Η/Υ Ακαδημαϊκό έτος ΤΕΤΡΑΔΙΟ ΕΡΓΑΣΤΗΡΙΟΥ #4

Προγραμματισμός ΙΙ (Java) 10. Πολυνηματικές εφαρμογές Τεκμηρίωση κώδικα

Αντικειμενοστραφής Προγραμματισμός I (5 ο εξ) Εργαστήριο #4 ο : Αποσφαλμάτωση (debugging), μετατροπές

ΕΡΓΑΣΤΗΡΙΟ 9: Συμβολοσειρές και Ορίσματα Γραμμής Εντολής

ΠΑΝΕΠΙΣΤΗΜΙΟ ΘΕΣΣΑΛΙΑΣ ΣΧΟΛΗ ΘΕΤΙΚΩΝ ΕΠΙΣΤΗΜΩΝ ΤΜΗΜΑ ΠΛΗΡΟΦΟΡΙΚΗΣ

«ΕΙΔΙΚΑ ΘΕΜΑΣΑ ΣΟΝ ΠΡΟΓΡΑΜΜΑΣΙΜΟ ΤΠΟΛΟΓΙΣΩΝ» Κεφάλαιο 4: Αντικειμενοςτρεφήσ Προγραμματιςμόσ

Μάθημα 3 ο ΔΙΕΡΓΑΣΙΕΣ (PROCESSES)

Τι χρειάζεται ένας φοιτητής για τη σωστή παρακολούθηση και συμμετοχή στο μαθημα;

Αντικειμενοστρεφής Προγραμματισμός

2.1. Εντολές Σχόλια Τύποι Δεδομένων

I (JAVA) Ονοματεπώνυμο: Α. Μ.: Δώστε τις απαντήσεις σας ΕΔΩ: Απαντήσεις στις σελίδες των ερωτήσεων ΔΕΝ θα ληφθούν υπ όψην.

public void printstatement() { System.out.println("Employee: " + name + " with salary: " + salary);

Εαρινό. Ύλη εργαστηρίου, Ασκήσεις Java

Προγραμματισμός Ι (ΗΥ120)

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Δημιουργία Κλάσεων και Αντικειμένων

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Constructors

Παράλληλη Επεξεργασία

Κλήση Συναρτήσεων ΚΛΗΣΗ ΣΥΝΑΡΤΗΣΕΩΝ. Γεώργιος Παπαϊωάννου ( )

public class ArrayStack implements Stack {

Λειτουργικά Συστήματα. Τ.Ε.Ι. Ιονίων Νήσων Σχολή Διοίκησης και Οικονομίας - Λευκάδα

Δομές ελέγχου ροής προγράμματος

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Πίνακες Κλάσεις και Αντικείμενα

Ανάπτυξη και Σχεδίαση Λογισμικού

ΠΑΝΕΠΙΣΤΗΜΙΟ ΠΕΙΡΑΙΩΣ ΣΧΟΛΗ ΤΕΧΝΟΛΟΓΙΩΝ ΠΛΗΡΟΦΟΡΙΚΗΣ ΚΑΙ ΕΠΙΚΟΙΝΩΝΙΩΝ ΤΜΗΜΑ ΨΗΦΙΑΚΩΝ ΣΥΣΤΗΜΑΤΩΝ «ΔΟΜΕΣ ΔΕΔΟΜΕΝΩΝ»

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Μέθοδοι

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Μέθοδοι

Λύσεις για τις ασκήσεις του lab5

Λειτουργικά Συστήματα Η/Υ

ΧΡΟΝΟΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ

B. Ενσωμάτωση Ιθαγενών Μεθόδων

ΥΠΟΠΡΟΓΡΑΜΜΑΤΑ. Κάθε υποπρόγραμμα έχει μόνο μία είσοδο και μία έξοδο. Κάθε υποπρόγραμμα πρέπει να είναι ανεξάρτητο από τα άλλα.

Διδάσκων: Παναγιώτης Ανδρέου

Τύποι Δεδομένων και Απλές Δομές Δεδομένων. Παύλος Εφραιμίδης V1.0 ( )

1. Πότε χρησιμοποιούμε την δομή επανάληψης; Ποιες είναι οι διάφορες εντολές (μορφές) της;

12.6. Άσκηση 6 - [αξιοποίηση γραφικής διεπαφής (GUI)] (έκδοση 2004)

1. Εισαγωγή. Λειτουργικά Συστήματα Η/Υ. Διεργασίες. Ορισμός ΚΕΦΑΛΑΙΟ 3 - ΔΙΕΡΓΑΣΙΕΣ. Κεφάλαιο 3 «Διεργασίες»

Transcript:

13. Νήματα Ολοκληρώνοντας αυτό το κεφάλαιο θα μπορείτε Να κατανοείτε την έννοια του νήματος (thread) Να δημιουργείτε διακριτά νήματα στη Java, που θα ελέγχουν τον κώδικα και τα δεδομένα που χρησιμοποιούνται από το νήμα αυτό Να ελέγχετε την εκτέλεση ενός νήματος και να γράφετε κώδικα ανεξάρτητο από την πλατφόρμα με χρήση νημάτων Να κατανοείτε ορισμένες από τις δυσκολίες που προκύπτουν όταν πολλά νήματα διαμοιράζονται δεδομένα Να κατανοείτε τη χρήση της δεσμευμένης λέξης synchronized στη Java για την προστασία των δεδομένων από καταστροφή. 13.1 Νήματα και χρήση νημάτων στη Java 13.1.1. Τι είναι τα νήματα; Μία απλή, αλλά χρήσιμη, άποψη ενός υπολογιστή είναι ότι έχει μία CPU, που πραγματοποιεί κάποιους υπολογισμούς, κάποια ROM που περιέχει το πρόγραμμα που εκτελεί η CPU και κάποια RAM που περιέχει τα δεδομένα με βάση τα οποία λειτουργεί το πρόγραμμα. Σε αυτή την απλή άποψη υπάρχει μόνο μία εργασία που πραγματοποιείται σε κάθε χρονική στιγμή. Μία πιο πλήρης άποψη των περισσοτέρων σύγχρονων υπολογιστικών συστημάτων δίνει τη δυνατότητα να πραγματοποιούνται περισσότερες από μία εργασίες την ίδια χρονική στιγμή ή τουλάχιστον να έχουμε αυτή την αίσθηση. Σε αυτή τη φάση της συζήτησης δε μας ενδιαφέρει πώς επιτυγχάνεται αυτό το γεγονός, αλλά μόνο να λάβουμε υπόψη μας τα παρελκόμενα από προγραμματιστικής άποψης. Αν πραγματοποιούνται περισσότερες από μία εργασίες, είναι αντίστοιχο με το να έχουμε περισσοτέρους του ενός υπολογιστές. Για το κεφάλαιο αυτό θα θεωρήσουμε ότι ένα νήμα (thread), ή πλαίσιο εκτέλεσης ή συμφραζόμενα εκτέλεσης (execution context), είναι η οριοθέτηση μίας ιδεατής CPU με το δικό της κώδικα προγράμματος και τα δικά της δεδομένα. Η κλάση java.lang.thread στις βασικές βιβλιοθήκες της Java μάς επιτρέπει να δημιουργούμε και να ελέγχουμε τα νήματά μας. Στη συνέχεια θα γράφουμε Thread όταν αναφερόμαστε στην κλάση java.lang.thread και νήμα όταν αναφερόμαστε στην ιδέα συμφραζομένων εκτέλεσης. 13.1.2. Τρία μέρη ενός νήματος Ένα νήμα αποτελείται από τρία μέρη. Πρώτα υπάρχει η ίδια η ιδεατή CPU. Δεύτερον υπάρχει ο κώδικας που εκτελεί η CPU. Τρίτον υπάρχουν τα δεδομένα στα οποία εφαρμόζεται ο κώδικας 1

CPU Ένα νήμα ή συμφραζόμενα εκτέλεσης Κώδικας Δεδομένα Στη Java στην κλάση Thread εμπεριέχεται μία ιδεατή CPU. Όταν δημιουργείται ένα νήμα μεταβιβάζεται σε αυτό, μέσω των ορισμάτων της συνάρτησης δημιουργίας του, τα δεδομένα στα οποία θα πρέπει να λειτουργήσει. Είναι σημαντικό να τονίσουμε ότι αυτές οι τρεις απόψεις είναι ουσιαστικά ανεξάρτητες. Ένα νήμα μπορεί να εκτελεί τον ίδιο ή διαφορετικό κώδικα από ένα άλλο νήμα. Ένα νήμα μπορεί να έχει πρόσβαση στα ίδια ή σε διαφορετικά δεδομένα από ό,τι ένα άλλο νήμα. 13.1.3. Δημιουργία ενός νήματος Ας δούμε τον τρόπο με τον οποίο δημιουργείται ένα νήμα και να συζητήσουμε πώς χρησιμοποιούνται τα ορίσματα των συναρτήσεων δημιουργίας του για την παροχή του κώδικα και των δεδομένων για το νήμα κατά την εκτέλεσή του. Μία συνάρτηση δημιουργίας Thread δέχεται ένα όρισμα που είναι στιγμιότυπο ενός Runnable. Δηλαδή πρέπει να ορίσουμε μία κλάση η οποία θα υλοποιεί τη διεπαφή Runnable και στη συνέχεια να δημιουργήσουμε ένα στιγμιότυπο αυτής της κλάσης. Η αναφορά που προκύπτει είναι κατάλληλο όρισμα για τη συνάρτηση δημιουργίας που θέλουμε. Για παράδειγμα: public class Xyz implements Runnable { int i; public void run() { while (true) { System.out.println( Hello + i++); Μας επιτρέπει να δημιουργήσουμε ένα νήμα ως εξής: Runnable r = new Xyz(); Thread t = new Thread(r); Με δεδομένο αυτό έχουμε ένα νέο νήμα το οποίο εμπεριέχεται στην αναφορά του Thread t. Αυτό ετοιμάζεται για να εκτελέσει τον κώδικα που ξεκινά με τη μέθοδο run() της κλάσης Xyz. (Η διεπαφή Runnable απαιτεί την ύπαρξη μίας public void μεθόδου με το όνομα run().) Τα δεδομένα που χρησιμοποιεί το νήμα αυτό παρέχονται από το στιγμιότυπο της Xyz στο οποίο αναφερόμαστε ως r. 2

Το νέο νήμα Thread t CPU κλάση Xyz Κώδικας Δεδομένα Στιγμιότυπο r της Xyz Συνεπώς, εν περιλήψει, αναφερόμαστε σε ένα νήμα μέσω ενός στιγμιοτύπου του αντικειμένου Thread. Ο κώδικας που θα εκτελέσει αυτό το νήμα θα ληφθεί από την κλάση του αντικειμένου που μεταβιβάζεται στη συνάρτηση δημιουργίας του Thread. Η κλάση αυτή πρέπει να υλοποιεί τη διεπαφή Runnable. Τα δεδομένα στα οποία λειτουργεί το νήμα αυτό λαμβάνονται από το συγκεκριμένο στιγμιότυπο του Runnable που μεταβιβάζεται στη συνάρτηση δημιουργίας του Thread. 13.1.4. Εκκίνηση ενός Thread Αν και έχουμε δημιουργήσει ένα νήμα αυτό δεν ξεκινά να εκτελείται άμεσα. Για να το ξεκινήσουμε χρησιμοποιούμε τη μέθοδο start(). Η μέθοδος αυτή είναι στην κλάση Thread συνεπώς, με βάση τα πιο πάνω, λέμε απλά: t.start() Στο σημείο αυτό η ιδεατή CPU που βρίσκεται μέσα στο νήμα γίνεται εκτελέσιμη. Μπορείτε να το φανταστείτε σα να δίνετε ρεύμα στην ιδεατή CPU. 13.1.5. Χρονοπρογραμματισμός του νήματος Αν και το νήμα γίνεται εκτελέσιμο, αυτό δε σημαίνει απαραίτητα ότι ξεκινά άμεσα. Σε έναν υπολογιστή με μία μόνο CPU (πραγματική CPU), σαφώς μπορούμε να κάνουμε μόνο ένα πράγμα κάθε φορά. Ας δούμε τώρα τον τρόπο με τον οποίο κατανέμεται η CPU όταν θα μπορούσαν να εκτελούνται περισσότερα από ένα νήματα. Στη Java τα νήματα είναι προ-εκχωρητικά (pre-emptive), αλλά δεν εκτελούνται κατ ανάγκη με βάση χρονομερίδια (time-sliced). (Είναι ένα κοινό λάθος να πιστεύουμε ότι η προ-εκχώρηση ταυτίζεται με το χρονοπρογραμματισμό με βάση τα χρονομερίδια.) Το μοντέλο του προ-εκχωρητικού χρονοπρογραμματιστή είναι ότι πολλά νήματα μπορεί να είναι έτοιμα προς εκτέλεση, αλλά μόνο ένα εκτελείται στην πράξη. Η διεργασία αυτή θα συνεχίσει να εκτελείται μέχρις ότου είτε πάψει να είναι εκτελέσιμη είτε μία άλλη διεργασία υψηλότερης προτεραιότητας γίνει εκτελέσιμη. Στην περίπτωση αυτή λέμε ότι το νήμα χαμηλότερης προτεραιότητας προ-εκχωρείται από το νήμα υψηλότερης προτεραιότητας. Ένα νήμα μπορεί να πάψει να είναι διαθέσιμο για πλήθος λόγων. Μπορεί να έχει εκτελέσει μία κλήση Thread.sleep(), επί τούτου για να ζητήσει να παύσει για μία χρονική περίοδο. Μπορεί να χρειάζεται να περιμένει για κάποια αργή εξωτερική συσκευή, ίσως κάποιον δίσκο ή το χρήστη. 3

Όλα τα νήματα που είναι εκτελέσιμα αλλά δεν εκτελούνται διατηρούνται σε ουρές ανάλογα με την προτεραιότητά τους. Το πρώτο νήμα στη μη-κενή ουρά με την υψηλότερη προτεραιότητα θα εκτελεστεί. Όταν ένα νήμα πάψει να εκτελείται εξ αιτίας της προ-εκχώρησης, φεύγει από την εκτελούμενη κατάσταση και τοποθετείται στο τέλος της εκτελέσιμης ουράς. Με παρόμοιο τρόπο ένα νήμα που γίνεται εκτελέσιμο αφού έχει απενεργοποιηθεί (blocked) (απλή απενεργοποίηση ή πιθανή αναμονή για είσοδο/έξοδο) πάει πάντα στο τέλος της ουράς. Δεδομένου του ότι τα νήματα στη Java δεν χρησιμοποιούν κατ ανάγκη χρονομερίδια, και με την έλλειψη περισσότερο προηγμένης κατανόησης, και της κατάλληλης σχεδίασης του προγράμματος, πρέπει να είστε βέβαιοι ότι ο κώδικας για τα νήματά σας δίνει στα άλλα νήματα την ευκαιρία να εκτελεσθούν από καιρού εις καιρόν. Αυτό μπορεί να επιτευχθεί δίνοντας περιοδικά μία κλήση sleep(), όπως στον κεντρικό βρόχο. public class Xyz implements Runnable { public void run() { while (true) { // διάφορα ενδιαφέροντα πράγματα... // Ας δουλέψουν και οι άλλοι try { Thread.sleep(10); catch (InterruptedException e) { // το νήμα διακόπηκε από άλλο Παρατηρήστε τη χρήση των try και catch. Επίσης σημειώστε ότι η κλήση sleep() είναι μία static μέθοδος της κλάσης Thread και συνεπώς αναφερόμαστε σε αυτή ως Thread.sleep(x). Το όρισμα καθορίζει το ελάχιστο αριθμό milliseconds κατά τα οποία το νήμα θα παραμείνει ανενεργό. Η εκτέλεση του νήματος δε θα συνεχισθεί παρά μόνο όταν περάσει αυτή η χρονική περίοδος συν κάποια χρόνο στον οποίο τα υπόλοιπα νήματα θα απενεργοποιηθούν, επιτρέποντας στο νήμα να ξεκινήσει στην πράξη. Μία άλλη μέθοδος στην κλάση Thread, η yield() μπορεί να χρησιμοποιηθεί για να δώσει στα άλλα νήματα την ευκαιρία να εκτελεστούν χωρίς να σταματήσει στην πράξη το τρέχον νήμα, παρά μόνο αν αυτό είναι απαραίτητο. Αν είναι εκτελέσιμα άλλα νήματα της ίδιας προτεραιότητας η yield() τοποθετεί το νήμα που την κάλεσε στο τέλος της εκτελέσιμης ουράς και επιτρέπει να ξεκινήσει ένα άλλο νήμα. Αν δεν υπάρχουν εκτελέσιμα νήματα στην ίδια προτεραιότητα η yield() δεν κάνει τίποτα. Σημειώστε ότι μία κλήση στη sleep() μπορεί να επιτρέψει την εκτέλεση νημάτων σε χαμηλότερη προτεραιότητα. Η μέθοδος yield() δίνει την ευκαιρία για εκτέλεση μόνο στα νήματα ίδιας προτεραιότητας. 4

13.2 Βασικός έλεγχος νημάτων 13.2.1. Τερματισμός ενός νήματος Deprecated Όταν επιστρέφει ένα νήμα από το τέλος μίας μεθόδου run(), αυτό πεθαίνει. Μετά από αυτό δεν μπορεί να ξανα-εκτελεστεί. Ένα νήμα μπορεί να σταματήσει με τη βία δίνοντας την μέθοδο stop(). Αυτή πρέπει να χρησιμοποιηθεί σε συγκεκριμένο στιγμιότυπο της κλάσης Thread για παράδειγμα: public class Xyz implements Runnable { // Διάφορα πράγματα που πρέπει να γίνονται σε ένα νήμα public class ttest { public static void main (String args[]) { Runnable r = new Xyz(); Thread t = new Thread(r); t.start(); // άλλα πράγματα if (time_to_kill) { t.stop(); Μέσα ένα συγκεκριμένο τμήμα κώδικα είναι δυνατό να αποκτήσουμε αναφορά στο τρέχον νήμα χρησιμοποιώντας τη στατική μέθοδο της Thread currentthread(), για παράδειγμα: public class Xyz implements Runnable { public void run() { while (true) { // διάφορα ενδιαφέροντα if (time_to_die) { Thread.currentThread().stop(); Σημειώστε ότι στην περίπτωση αυτή η εκτέλεση της stop() θα καταστρέψει το τρέχον πλαίσιο εκτέλεσης και συνεπώς δε θα εκτελεστεί η συνέχεια του βρόχου run() σε αυτό το πλαίσιο. Η χρήση της Thread.currentThread() δεν περιορίζεται στο παράδειγμα της stop()! 13.2.2. Έλεγχος ενός νήματος Ορισμένες φορές είναι δυνατό ένα νήμα να είναι σε άγνωστη κατάσταση (αυτό μπορεί να συμβεί αν ο κώδικάς σας δεν ελέγχει απ ευθείας ένα συγκεκριμένο νήμα). Είναι δυνατό να ρωτήσουμε αν ένα νήμα είναι ακόμα «ζωντανό» χρησιμοποιώντας τη μέθοδο 5

isalive(). Το να είναι ένα νήμα «ζωντανό» δε σημαίνει απαραίτητα ότι εκτελείται, μόνο ότι έχει ξεκινήσει και δεν έχει κληθεί για αυτό ποτέ η μέθοδος stop() ούτε έχει φτάσει στο τέλος της μεθόδου run(). 13.2.3. Βάζοντας τα νήματα σε αναμονή Υπάρχουν αρκετοί μηχανισμοί για να σταματήσουμε προσωρινά την εκτέλεση ενός νήματος. Ακολουθώντας αυτόν τον τύπο απενεργοποίησης η εκτέλεση μπορεί να ξαναξεκινήσει σα να μην είχε συμβεί τίποτα, απλά το νήμα φαίνεται να είχε εκτελέσει μία εντολή πολύ αργά. sleep() Η μέθοδος sleep() παρουσιάστηκε νωρίτερα και χρησιμοποιείται για να σταματήσουμε ένα νήμα για μία χρονική περίοδο. Θυμηθείτε ότι συνήθως το νήμα δε θα ξαναξεκινήσει αμέσως μόλις τελειώσει η περίοδος απενεργοποίησης. Αυτό ισχύει γιατί μπορεί ένα άλλο νήμα να εκτελείται τη χρονική αυτή στιγμή και να μην μπορεί να φύγει από το χρονοπρογραμματισμό εκτός και αν (α) το νήμα που αφυπνίζεται να είναι υψηλότερης προτεραιότητας, (β) το εκτελούμενο νήμα να απενεργοποιηθεί για κάποιον λόγο ή (γ) να είναι ενεργοποιημένος ο χρονοπρογραμματισμός με βάση χρονομερίδια. Στις περισσότερες περιπτώσεις καμία από τις δύο τελευταίες περιπτώσεις δε θα συμβεί άμεσα. suspend() και resume() Deprecated Ορισμένες φορές είναι κατάλληλο να απενεργοποιήσετε την εκτέλεση ενός νήματος επ αόριστον, οπότε κάποιο άλλο νήμα θα πρέπει να είναι υπεύθυνο για να ξεκινήσει ξανά την εκτέλεση. Για το λόγο αυτό χρειαζόμαστε ένα ζεύγος μεθόδων Thread. Ονομάζονται suspend() και resume(). Για παράδειγμα: public class Xyz implements Runnable { public void run() { // διάφορα ενδιαφέροντα // τώρα περίμενε μέχρι να σου πουν να συνεχίσεις Thread.currentThread().suspend(); // κάνε άλλα πράγματα...... Runnable r = new Xyz(); Thread t = new Thread(r); t.start(); // παύση για λίγο για να εκτελεστεί το Xyz // υπέθεσε ότι έχει φτάσει στο suspend() Thread.sleep(1000); // ξανακάνει το Xyz εκτελέσιμο 6

t.resume(); // άφησέ το να εκτελεσθεί Thread.yields(); Σημειώστε ότι εν γένει ένα Thread μπορεί να απενεργοποιηθεί από οποιοδήποτε τμήμα κώδικα που μπορεί να το χειριστεί δηλαδή έχει μία μεταβλητή που αναφέρεται σε αυτό. Σαφώς όμως ένα Thread μπορεί να ξανα-ξεκινήσει μόνο από ένα Thread άλλο από το ίδιο, καθώς το απενεργοποιημένο Thread δεν εκτελεί κανένα κώδικα! join() Η μέθοδος join() προκαλεί το τρέχον νήμα να περιμένει μέχρι να τερματίσει το νήμα στο οποίο κλήθηκε η μέθοδος join. Για παράδειγμα: TimerThread tt = new TimerThread(100); tt.start();... public void timeout() { // Περίμενε να τελειώσει το TimerThread tt.join(); // Τώρα συνέχισε σε αυτό το νήμα... Η join μπορεί να κληθεί επίσης με μία τιμή timeout σε milliseconds: void join(long timeout); όπου η μέθοδος join είτε θα απενεργοποιήσει το τρέχον νήμα για τα timeout milliseconds, είτε θα περιμένει μέχρι να τερματιστεί το νήμα στο οποίο κλήθηκε. 13.3 Άλλοι τρόποι δημιουργίας νημάτων Περιγράψαμε ήδη τη δημιουργία των νημάτων με χρήση μίας ξεχωριστής κλάσης η οποία υλοποιεί το Runnable. Στην πράξη δεν είναι η μόνη πιθανή προσέγγιση. Ο ορισμός της κλάσης Thread δηλώνει ότι στην πράξη υλοποιεί τη διεπαφή Runnable. Είναι δυνατό να δημιουργήσετε ένα Thread δημιουργώντας μία κλάση η οποία κάνει extends την κλάση Thread, αντί να υλοποιήσετε τη Runnable. public class MyThread extends Thread { public void run() { while (running) { // do lots of interesting stuff sleep(100); public static void main(string args[]) { Thread t = new MyThread(); t.start(); 7

Εδώ υπάρχει μόνο μία κλάση η MyThread. Όταν δημιουργείται ένα αντικείμενο Thread δεν παρέχονται ορίσματα. Αυτή η μορφή συνάρτησης δημιουργίας δημιουργεί ένα Thread που χρησιμοποιεί τη δική του ενσωματωμένη μέθοδο run(). 13.3.1. Ποιον τρόπο να χρησιμοποιήσουμε; Δεδομένης της επιλογής ανάμεσα στις δύο προσεγγίσεις πώς θα επιλέξετε ανάμεσά τους; Υπάρχουν σημεία υπέρ κάθε μίας προσέγγισης: Υπέρ της υλοποίησης της Runnable Από απόψεως αντικειμενοστρεφούς σχεδίασης η κλάση Thread είναι αυστηρά μία οριοθέτηση μίας ιδεατής CPU και ως τέτοια θα πρέπει να επεκτείνεται μόνο όταν η συμπεριφορά του μοντέλου της CPU αλλάζει ή επεκτείνεται με κάποιον τρόπο. Συνήθως δεν είναι αυτό εκείνο που θέλουμε να κάνουμε. Εξ αιτίας αυτού, και για να επιτύχουμε τη διάκριση ανάμεσα σε CPU, Κώδικα και Δεδομένα, στο παρόν κεφάλαιο επιλέξαμε την πρώτη προσέγγιση. Εφόσον η Java επιτρέπει μόνο απλή κληρονομικότητα δεν είναι δυνατό να επεκτείνουμε οποιαδήποτε άλλη κλάση, όπως για παράδειγμα την Applet, αν έχουμε ήδη επεκτείνει τη Thread. Σε ορισμένες περιπτώσεις, ο περιορισμός αυτός θα σας αναγκάσει να επιλέξετε την προσέγγιση υλοποίησης της Runnable. Εφόσον ορισμένες φορές αναγκάζεστε να χρησιμοποιήσετε την υλοποίηση της Runnable μπορεί να θέλετε να είστε συνεπής και να το κάνετε πάντα έτσι. Υπέρ της επέκτασης της Thread Όταν η μέθοδος run έχει ενσωματωθεί σε μία κλάση που κάνει extends την κλάση Thread, τότε η αναφορά this στην πράξη αναφέρεται στο πραγματικό στιγμιότυπο της Thread και όχι στον έλεγχο της εκτέλεσης. Εξ αιτίας αυτού δε χρειάζεται να γράφετε πλέον μεγάλες εντολές ελέγχου όπως: Thread.currentThread().start() αλλά αντ αυτής μπορείτε να δώσετε απλά: start(); Επειδή ο κώδικας που προκύπτει είναι λίγο απλούστερος, πολλοί προγραμματιστές Java χρησιμοποιούν το μηχανισμό της επέκτασης της Thread. Προσέξτε όμως ότι ο περιορισμός της απλής κληρονομικότητας στη συνέχεια μπορεί να οδηγήσει στη συνέχεια του κύκλου ζωής του λογισμικού σας σε δυσκολίες. 13.4 Χρήση της synchronized στη Java 13.4.1. Εισαγωγή Στο τμήμα αυτό θα συζητήσουμε τη χρήση της δεσμευμένης λέξης synchronized. Αυτή παρέχει στη Java ένα μηχανισμό για να επιτρέπει στους προγραμματιστές να ελέγχουν τα νήματα τα οποία διαμοιράζονται δεδομένα. 8

13.4.2. Το πρόβλημα Έστω μία κλάση που αναπαριστά μία στοίβα. Μία πρώτη έκδοσή της μπορεί να είναι η ακόλουθη: class stack { int idx = 0; char [] data = new char[6]; public void push (char c) { data[idx] = c; idx++; public char pop() { idx--; return data[idx]; Σημειώστε ότι η κλάση δεν κάνει καμία προσπάθεια να αντιμετωπίσει υπερχείλιση ή υποχείλιση και ότι η χωρητικότητά της είναι περιορισμένη. Τα ζητήματα αυτά δε θα μας απασχολήσουν στη συζήτησή μας. Παρατηρήστε ότι η συμπεριφορά του μοντέλου αυτού απαιτεί η τιμή του δείκτη να μας δίνει το δείκτη του πίνακα για το επόμενο άδειο κελί στη στοίβα και για το λόγο αυτό υιοθετεί την προσέγγιση «πρώτα μείωση, μετά αύξηση» (predecrement, postincrement). Φανταστείτε τώρα ότι δύο νήματα έχουν αμφότερα μία αναφορά στο ίδιο στιγμιότυπο της κλάσης. Το ένα νήμα τοποθετεί δεδομένα (push) στη στοίβα και το άλλο, λίγο-πολύ ανεξάρτητα, βγάζει (pop) δεδομένα από τη στοίβα. Στη γενική περίπτωση θα φαίνεται ότι τα δεδομένα θα τοποθετούνται και θα αφαιρούνται επιτυχώς. Υπάρχει όμως ένα εν δυνάμει πρόβλημα. Έστω ότι το νήμα «a» προσθέτει και το νήμα «b» αφαιρεί χαρακτήρες. Το νήμα «a» μόλις έχει αφήσει ένα χαρακτήρα, αλλά δεν έχει αυξήσει ακόμα το μετρητή του δείκτη. Για κάποιο λόγο το νήμα προ-εκχωρείται. Στο σημείο αυτό, το μοντέλο δεδομένων που αναπαρίσταται από το αντικείμενό μας είναι ασυνεπές. buffer = p q r idx = 2 ^ Συγκεκριμένα η συνέπεια απαιτεί είτε idx = 3 είτε να μην είχε προστεθεί ο χαρακτήρας. Το νήμα «a» συνεχίζει την εκτέλεσή του και μπορεί να μην υπήρξε πρόβλημα, αλλά έστω ότι και το νήμα «b» περίμενε για να αφαιρέσει ένα χαρακτήρα. Όσο το «a» περίμενε τη σειρά του να εκτελεσθεί, το βήμα «b» έχει την ευκαιρία του να εκτελεσθεί. Μπαίνοντας στη μέθοδο pop() έχουμε μία κατάσταση ασυνεπών δεδομένων. Η μέθοδος pop συνεχίζει και μειώνει την τιμή του δείκτη: buffer = p q r idx = 1 ^ 9

Με αυτό ουσιαστικά αγνοεί το χαρακτήρα r. Μετά από αυτό επιστρέφει το χαρακτήρα q. Μέχρι τώρα η συμπεριφορά είναι σα να μην τοποθετήθηκε ποτέ ο χαρακτήρας r, συνεπώς δεν μπορούμε να πούμε ότι υπάρχει κάποιο πρόβλημα. Αλλά ας δούμε τι θα συμβεί όταν επιστρέψει το αρχικό νήμα «a». Το νήμα «a» συνεχίζει από εκεί που έμεινε μέσα στη μέθοδο push(). Συνεχίζει και αυξάνει κατά ένα την τιμή του δείκτη. Τώρα έχουμε buffer = p q r idx = 2 ^ Παρατηρήστε ότι αυτή η διαμόρφωση υπονοεί ότι το q είναι επιτρεπτό και ότι το επόμενο κενό κελί είναι το κελί που περιέχει το r. Με άλλα λόγια, θα διαβάσουμε το q σα να είχε τοποθετηθεί δύο φορές στη στοίβα, αλλά δε θα δούμε ποτέ το r. Αυτό είναι ένα απλό παράδειγμα ενός γενικότερου προβλήματος που μπορεί να προκύψει όταν πολλά νήματα προσπελαύνουν διαμοιραζόμενα δεδομένα. Χρειαζόμαστε ένα μηχανισμό για να βεβαιωθούμε ότι τα δεδομένα μας προστατεύονται από τέτοιου είδους μη ελεγχόμενες προσπελάσεις. Μία προσέγγιση είναι να αποτρέψουμε το νήμα «a» από το να προ-εκχωρηθεί μέχρι να τελειώσει με το κρίσιμο τμήμα του κώδικα. Η προσέγγιση αυτή είναι κοινή στον προγραμματισμό υπολογιστών σε χαμηλό επίπεδο, αλλά εν γένει είναι μη κατάλληλη σε συστήματα με πολλούς χρήστες. Μία άλλη προσέγγιση, αυτή με την οποία λειτουργεί η Java, είναι να παρέχουμε ένα μηχανισμό με βάση τον οποίο τα δεδομένα θα αντιμετωπίζονται ως ευαίσθητα. 13.4.3. Η ένδειξη κλειδώματος Object Στη Java κάθε στιγμιότυπο οποιουδήποτε αντικειμένου έχει συσχετισμένο με αυτό μία ένδειξη (flag). Η ένδειξη αυτή μπορεί να θεωρηθεί ως μία «ένδειξη κλειδώματος». Για αλληλεπίδραση με την ένδειξη αυτή παρέχεται η δεσμευμένη λέξη synchronized. Δείτε το πιο κάτω τροποποιημένο τμήμα κώδικα: public void push (char c) { synchronized(this) { data[idx] = c; idx++; Όταν το νήμα φτάσει στην εντολή synchronized εξετάζει το αντικείμενο που μεταβιβάζεται ως όρισμα και προσπαθεί να «πάρει» την ένδειξη κλειδώματός του. 10

Αντικείμενο this Κώδικας ή συμπεριφορά Νήμα πριν το synchronized(this) public void push (char c) { synchronized(this) { data[idx] = c; idx++; Δεδομένα ή κατάσταση Αντικείμενο this Κώδικας ή συμπεριφορά Νήμα μετά το synchronized(this) public void push (char c) { synchronized(this) { data[idx] = c; idx++; Δεδομένα ή κατάσταση Είναι σημαντικό να κατανοήσουμε ότι από μόνη της η λύση αυτή δεν προστατεύει τα δεδομένα στο αντικείμενο this. Αν κληθεί από άλλο νήμα η μέθοδος pop() χωρίς να τροποποιηθεί συνεχίζει να υπάρχει κίνδυνος να καταστραφεί η συνέπεια του αντικειμένου this. Αυτό που πρέπει να κάνουμε είναι να τροποποιήσουμε και την pop με τον ίδιο τρόπο. Προσθέτουμε μία κλήση στη synchronized(this) γύρω από τα ευαίσθητα τμήματα της κλήσης της pop() ακριβώς παράλληλα με τον τρόπο που το κάναμε για τη μέθοδο push(). Το επόμενο σχήμα δείχνει τι θα συμβεί αν ένα άλλο νήμα προσπαθήσει να εκτελέσει τη μέθοδο ενώ το αρχικό νήμα μας διατηρεί την ένδειξη κλειδώματος. 11

Αντικείμενο this Κώδικας ή συμπεριφορά Δεδομένα ή κατάσταση zzz Νήμα πριν το synchronized(this) public char pop () { synchronized(this) { idx--; return data[idx]; Όταν το νήμα προσπαθήσει να εκτελέσει την εντολή synchronized(this) προσπαθεί να πάρει την ένδειξη κλειδώματος του αντικειμένου this. Εφόσον η ένδειξη δεν είναι παρούσα, το νήμα δεν μπορεί να συνεχίσει την εκτέλεσή του. Στην πράξη πηγαίνει σε μία ουρά με νήματα που αναμένουν. Η ουρά αυτή σχετίζεται με την ένδειξη κλειδώματος του αντικειμένου έτσι ώστε όταν η ένδειξη επιστραφεί στο αντικείμενο, το πρώτο νήμα που την ανέμενε να την λάβει και να συνεχίσει να εκτελείται. 13.4.4. Ελευθέρωση της ένδειξης κλειδώματος Καθώς το νήμα που αναμένει την ένδειξη κλειδώματος ενός αντικειμένου δε θα συνεχίσει να εκτελείται παρά μόνο όταν αυτή επιστραφεί από το νήμα που την κρατά, είναι σαφώς σημαντικό να επιστραφεί η ένδειξη όταν αυτή δε χρειάζεται πλέον. Η ένδειξη κλειδώματος δίνεται στην πράξη πίσω στο αντικείμενο αυτόματα όταν το νήμα που την κρατά περάσει το τέλος του μπλοκ που σχετίζεται με την κλήση synchronized() μέσω της οποία έλαβε την ένδειξη. Η Java δίνει μεγάλη προσοχή ώστε η ένδειξη να επιστρέφει πάντα σωστά, συνεπώς αν ένα συγχρονισμένο μπλοκ παράγει μία εξαίρεση ή αν υπάρχει ένα break σε βρόχο που θα βγει έξω από το μπλοκ, η ένδειξη θα επιστραφεί σωστά. Επίσης, αν ένα νήμα κάνει δύο φορές κλήση synchronized στο ίδιο αντικείμενο, η ένδειξη θα απελευθερωθεί σωστά στην έξοδο από το εξώτερο μπλοκ ενώ το ενδότερο ουσιαστικά αγνοείται. Οι κανόνες αυτοί κάνουν τα συγχρονισμένα μπλοκ πιο απλά στο χειρισμό από ισοδύναμες διευκολύνσεις σε άλλα συστήματα όπως οι δυαδικοί σημαφόροι. 13.4.5. Βάζοντάς τα όλα μαζί Όπως αναφέραμε και νωρίτερα ο μηχανισμός synchronized() λειτουργεί μόνο αν ο προγραμματιστής τοποθετήσει τις κλήσεις στις σωστές θέσεις. Τώρα θα δούμε πώς να δημιουργήσουμε μία καλά προστατευόμενη κλάση. Θεωρήστε την προσβασιμότητα των στοιχείων δεδομένων που αποτελούν τα ευαίσθητα μέρη του αντικειμένου. Αν αυτά δεν έχουν δηλωθεί ως ιδιωτικά, τότε μπορεί να τα προσπελάσει οποιοσδήποτε εξωτερικός κώδικας. Στην περίπτωση αυτή, βασιζόμαστε ότι ποτέ κανένας προγραμματιστής δε θα παραλείψει τις προστασίες που απαιτούνται. 12

Σαφώς αυτή δεν είναι μία ιδιαίτερα ασφαλής στρατηγική. Για το λόγο αυτό τα δεδομένα θα πρέπει να δηλώνονται πάντα ως ιδιωτικά. Εφόσον έχουμε δείξει ότι τα δεδομένα πρέπει, στην πράξη, να είναι σημειωμένα ως ιδιωτικά, το όρισμα στην κλήση synchronized() θα πρέπει να είναι το this. Εξ αιτίας αυτής της γενίκευσης η Java επιτρέπει μία συντομογραφία ώστε αντί να γράφουμε public void push (char c) { synchronized(this) { : : να μπορούμε να γράψουμε public synchronized void push (char c) { : : με το ίδιο αποτέλεσμα. Γιατί θα θέλαμε να επιλέξουμε τη μία αντί της άλλης; Αν χρησιμοποιούμε το synchronized ως τροποποιητή μεθόδου τότε ολόκληρη η μέθοδος γίνεται ένα συγχρονισμένο μπλοκ, πράγμα που μπορεί να έχει ως αποτέλεσμα ότι η ένδειξη κλειδώματος θα κρατηθεί περισσότερο απ όσο αυστηρά χρειάζεται. Αυτό μπορεί να είναι μη αποδοτικό. Από την άλλη, σημειώνοντας έτσι τη μέθοδο επιτρέπουμε στους χρήστες της να γνωρίζουν ότι μέσα σε αυτή υπάρχει συγχρονισμός, κάτι που μπορεί να είναι σημαντικό όταν σχεδιάζουμε ενάντια σε ένα αδιέξοδο, όπως θα δούμε στη συνέχεια. Σημειώστε ότι η γεννήτρια τεκμηρίωσης javadoc θα προωθήσει την ύπαρξη του τροποποιητή synchronized στα αρχεία τεκμηρίωσης, αλλά δε θα τεκμηριώσει τη χρήση του synchronized(this). 13.4.6. Αδιέξοδο Σε προγράμματα στα οποία πολλά νήματα ανταγωνίζονται για πρόσβαση σε πολλούς πόρους υπάρχει η πιθανότητα μίας συνθήκης η οποία είναι γνωστή ως αδιέξοδο (deadlock). Αυτό συμβαίνει όταν ένα νήμα αναμένει για μία ένδειξη κλειδώματος που κατέχει ένα άλλο νήμα, αλλά το άλλο νήμα αναμένει για μία ένδειξη κλειδώματος που κατέχει ήδη το πρώτο νήμα. Στην περίπτωση αυτή κανένα από τα δύο δεν μπορεί να συνεχίσει μέχρι το άλλο να έχει περάσει το τέρμα του συγχρονισμένου μπλοκ. Και καθώς κανένα από τα δύο δεν μπορεί να προχωρήσει, κανένα από τα δύο δε θα φτάσει στο τέρμα του μπλοκ. Η Java ούτε ανιχνεύει ούτε προσπαθεί να αποφύγει αυτή τη συνθήκη, είναι συνεπώς ευθύνη του προγραμματιστή να βεβαιωθεί ότι δεν πρόκειται να συμβεί. Ένας πρακτικός κανόνας για αποφυγή αδιεξόδου είναι ο ακόλουθος. Αν υπάρχουν πολλά αντικείμενα στα οποία θέλετε να αποκτήσετε συγχρονισμένη πρόσβαση, πάρτε μία συνολική απόφαση σχετικά με τη σειρά με την οποία θα αποκτάτε τις ενδείξεις κλειδώματος και ακολουθήστε την σε ολόκληρο το πρόγραμμα. 13

Μία πιο λεπτομερής συζήτηση του προβλήματος αυτού είναι πέρα από τους σκοπούς του μαθήματος. 13.5 Αλληλεπίδραση νημάτων wait() και notify() 13.5.1. Εισαγωγή Συχνά, δημιουργούνται διαφορετικά νήματα ειδικά για να πραγματοποιήσουν εργασίες που δε σχετίζονται μεταξύ τους. Ορισμένες φορές όμως οι εργασίες που πραγματοποιούν τελικά σχετίζονται με κάποιον τρόπο. Όταν συμβαίνει αυτό είναι απαραίτητο να προγραμματίσουμε κάποια αλληλεπίδραση ανάμεσα στα νήματα. Υπάρχουν πρότυποι τρόποι για να το κάνουμε αυτό και δεδομένου ενός μηχανισμού οι υπόλοιποι μπορούν να υλοποιηθούν χρησιμοποιώντας τον. Το τμήμα αυτό εξετάζει το μηχανισμό που παρέχει η Java και περιγράφει τη χρήση του. 13.5.2. Το πρόβλημα Γιατί λοιπόν να θέλουν δύο νήματα να αλληλεπιδράσουν; Ως απλό παράδειγμα θεωρήστε δύο ανθρώπους, ο ένας πλένει και ο άλλος σκουπίζει πιάτα. Αυτοί οι άνθρωποι αναπαριστούν τα νήματά μας. Ανάμεσά τους βρίσκεται ένα διαμοιραζόμενο αντικείμενο, η στεγνώστρα. Και οι δύο άνθρωποι είναι λίγο τεμπέληδες και θα προτιμούσαν να κάθονται αν δεν έχουν να κάνουν κάτι. Σαφώς αυτός που σκουπίζει δεν μπορεί να ξεκινήσει να σκουπίσει αν δεν υπάρχει τουλάχιστον ένα αντικείμενο στη στεγνώστρα. Επίσης, αν η στεγνώστρα γεμίσει ο πλύστης δεν μπορεί να συνεχίσει μέχρι να υπάρξει χώρος. Θα εισαγάγουμε τώρα το μηχανισμό που παρέχει η Java για την αποδοτική αντιμετώπιση αυτού του προβλήματος. 13.5.3. Η λύση Θα ήταν δυνατό να προγραμματίσουμε μία λύση σε αυτό το σενάριο χρησιμοποιώντας τις μεθόδους suspend() και resume(). Όμως, μία τέτοια λύση θα απαιτούσε τα δύο νήματα να δημιουργηθούν σε συνεργασία, καθώς κάθε ένα απαιτεί ένα χειριστήριο προς το άλλο. Εξ αιτίας αυτού η Java παρέχει ένα μηχανισμό επικοινωνίας που βασίζεται στα στιγμιότυπα των αντικειμένων. Κάθε στιγμιότυπο αντικειμένων στη Java έχει συσχετισμένες μαζί του δύο ουρές νημάτων. Η πρώτη χρησιμοποιείται από τα νήματα που θέλουν να αποκτήσουν την ένδειξη κλειδώματος και την παρουσιάσαμε νωρίτερα. Η δεύτερη ουρά χρησιμοποιείται για να υλοποιηθούν οι μηχανισμοί επικοινωνίας wait() και notify(). Στη βασική κλάση java.lang.object ορίζονται τρεις μέθοδοι. Αυτές είναι οι wait(), notify() και notifyall(). Θα ασχοληθούμε με τις δύο πρώτες. Ας επιστρέψουμε στο παράδειγμα του πλυσίματος πιάτων. Το νήμα a πλένει και το νήμα b σκουπίζει. Και τα δύο έχουν πρόσβαση στο αντικείμενο της στεγνώστρας. Έστω ότι το νήμα b το νήμα που σκουπίζει θέλει να σκουπίσει ένα αντικείμενο, αλλά η στεγνώστρα είναι άδεια. Αυτό μπορεί να γραφεί ως εξής: if (drainingboard.isempty()) drainingboard.wait(); 14

Τώρα όταν το νήμα b κάνει μία κλήση wait(), παύει να είναι εκτελέσιμο, και πηγαίνει στην ουρά αναμονής για το αντικείμενο της στεγνώστρας. Δε θα ξαναεκτελεστεί μέχρι κάτι να το βγάλει από αυτή την ουρά. Πώς λοιπόν θα ξαναξεκινήσει το στέγνωμα. Είναι ευθύνη του νήματος που πλένει να το ενημερώσει ότι υπάρχει κάτι χρήσιμο για να κάνει. Αυτό επιτυγχάνεται καλώντας τη notify() στη στεγνώστρα ως εξής: drainingboard.additem(plate); drainingboard.notify(); Στο σημείο αυτό το πρώτο νήμα που είχε απενεργοποιηθεί στην ουρά αναμονής της στεγνώστρας αφαιρείται από αυτή την ουρά και μπορεί να ανταγωνιστεί για εκτέλεση. Σημειώστε ότι η κλήση notify() γίνεται στην περίπτωση αυτή χωρίς να ελέγξει αν υπάρχουν νήματα που αναμένουν ή όχι. Η προσέγγιση αυτή δημιουργεί σαφώς άχρηστες κλήσεις. Είναι πιθανό να κάνουμε την κλήση μόνο αν το πιάτο που τοποθετήσαμε κάνει τη στεγνώστρα να πάψει να είναι άδεια. Αλλά αυτό είναι μία λεπτομέρεια και μάς απομακρύνει από την ουσία της συζήτησης αυτής. Επιπρόσθετα είναι σημαντικό να κατανοήσουμε ότι αν κληθεί η notify() χωρίς να υπάρχουν απενεργοποιημένα νήματα στην ουρά αναμονής, τότε η κλήση δεν έχει καμία επίπτωση. Οι κλήσεις στη notify() δεν αποθηκεύονται. Επιπρόσθετα θα πρέπει να γνωρίζετε ότι η notify() απελευθερώνει το πολύ το πρώτο νήμα στην ουρά αναμονής. Αν υπήρχαν περισσότερα από ένα μηνύματα που περίμεναν, τα υπόλοιπα συνεχίζουν να παραμένουν στην ουρά. Μπορεί να χρησιμοποιηθεί η μέθοδος notifyall() για να απελευθερωθούν όλα τα νήματα που περιμένουν, εφόσον αυτή είναι η συμπεριφορά που απαιτεί η σχεδίαση του συστήματος. Χρησιμοποιώντας το μηχανισμό αυτό μπορούμε να συντονίσουμε τα νήματα πλυσίματος και στεγνώματος αρκετά απλά, και χωρίς να χρειάζεται να γνωρίζουμε την ταυτότητά τους. Κάθε φορά που πραγματοποιούμε μία πράξη η οποία μάς εγγυάται ότι το άλλο νήμα μπορεί να κάνει κάποια σημαντική δουλειά, κάνουμε μία notify() στο αντικείμενο της στεγνώστρας. Κάθε φορά που προσπαθούμε να κάνουμε μια δουλειά αλλά δεν μπορούμε να συνεχίσουμε γιατί η στεγνώστρα είναι είτε γεμάτη είτε άδεια, κάνουμε wait() στο αντικείμενο της στεγνώστρας. 13.5.4. Η αλήθεια! Οι μηχανισμοί που περιγράψαμε μέχρι τώρα είναι πολλοί ωραίοι στις αρχές τους, αλλά στη Java η υλοποίηση δεν είναι πάντα τόσο απλή όσο υποθέτουμε. Συγκεκριμένα, η ίδια η ουρά αναμονής είναι μία ευαίσθητη δομή δεδομένων και συνεπώς χρειάζεται να την προστατεύουμε χρησιμοποιώντας το μηχανισμός συγχρονισμού. Δε χρειάζεται να ασχοληθούμε με τη λεπτομέρεια του γιατί αυτό συμβαίνει, αλλά είναι σημαντικό να γνωρίζουμε ότι πριν από κάθε κλήση wait(), notify() και notifyall() σε ένα αντικείμενο, θα πρέπει να κρατάμε την ένδειξη κλειδώματος για το αντικείμενο αυτό. Συγκεκριμένα πρέπει να καλούμε αυτές τις μεθόδους μέσα από ένα μπλοκ synchronized. Συνεπώς πρέπει ο κώδικάς μας να αλλάξει ως εξής: synchronized(drainingboard) { if (drainingboard.isempty()) drainingboard.wait(); 15

και με παρόμοιο τρόπο synchronized(drainingboard) { drainingboard.additem(plate); drainingboard.notify(); Μπορείτε αυτό να το εκλάβετε ως έναν απλό κανόνα της γλώσσας, αλλά δημιουργεί μία ενδιαφέρουσα παρατήρηση. Εφόσον γνωρίζουμε ότι η εντολή synchronized απαιτεί από το νήμα να λάβει την ένδειξη κλειδώματος προτού συνεχίσει, θα μπορούσαμε να φανταστούμε ότι μπορεί το νήμα πλυσίματος να μην φτάσει ποτέ την εντολή notify() αν το νήμα στεγνώματος έχει απενεργοποιηθεί σε κατάσταση wait(). Στην πράξη η υλοποίηση είναι τέτοια που αυτό δε συμβαίνει. Συγκεκριμένα, όταν καλούμε τη wait() αυτή πρώτα επιστρέφει στο αντικείμενο την ένδειξη κλειδώματος του αντικειμένου. Όμως για να αποφύγουμε την πιθανότητα κάποιας καταστροφής, όταν ένα νήμα δεχτεί τη notify() δε γίνεται αμέσως εκτελέσιμο, αλλά τοποθετείτε στην ουρά της ένδειξης κλειδώματος. Έτσι δεν μπορεί να συνεχίσει στην πράξη, αν δεν ξαναπάρει την ένδειξη κλειδώματος. Μία άλλη άποψη της πραγματικής υλοποίησης είναι ότι η μέθοδος wait() μπορεί να τερματιστεί είτε από μία notify() είτε καλώντας τη μέθοδο interrupt() της Thread. Στην περίπτωση αυτή, η wait() «πετάει» μία InterruptedException, η οποία συνήθως απαιτεί να τοποθετηθεί η wait() μέσα σε μία δομή try/catch. 13.6 Βάζοντάς τα όλα μαζί Ας προσπαθήσουμε τώρα να φτιάξουμε ένα πραγματικό παράδειγμα από όλα αυτά. Θα δουλέψουμε με τη βασική αρχή του πλυσίματος και του στεγνώματος, αλλά αντί για πιάτα στη στεγνώστρα θα μεταβιβάζουμε χαρακτήρες μέσω ενός αντικειμένου στοίβας. Στην επιστήμη των υπολογιστών αυτό είναι ένα κλασικό παράδειγμα του προβλήματος παραγωγών-καταναλωτών (producer-consumer problem). Θα ξεκινήσουμε βλέποντας τη μορφή του αντικειμένου της στοίβας, στη συνέχεια θα εξετάσουμε τις λεπτομέρειες των νημάτων που θα παράγουν και θα καταναλώνουν. Τέλος, θα δούμε τις λεπτομέρειες της στοίβας και τους μηχανισμούς που χρησιμοποιούνται για να την προστατέψουμε και να υλοποιήσουμε την επικοινωνία ανάμεσα στα νήματα. Η κλάση στοίβας, που θα ονομάζεται SyncStack για να τη διακρίνουμε από τη βασική κλάση java.util.stack, θα παρέχει την πιο κάτω δημόσια διεπαφή API: public void push(char c); public char pop(); 13.6.1. Παραγωγός Το νήμα του παραγωγού θα εκτελεί την πιο κάτω μέθοδο public void run() { char c; for (int i = 0; i < 20; i++) { c = (char) (Math.random() * 26 + A ); thestack.push ; 16

System.out.println( Produced: + c); try { Thread.sleep((int)(Math.random() * 100)); catch (InterruptedException e) { // αγνοήστε την Αυτή θα παράγει 20 τυχαία κεφαλαία λατινικά γράμματα και θα τα κάνει push σε μία στοίβα με τυχαία καθυστέρηση ανάμεσα σε κάθε τοποθέτηση. Η καθυστέρηση θα είναι μεταξύ 0 και 100 milliseconds. Κάθε χαρακτήρας που θα τοποθετείτε θα αναφέρεται στην κονσόλα. 13.6.2. Καταναλωτής Το νήμα του καταναλωτή θα εκτελεί την πιο κάτω μέθοδο: public void run() { char c; for (int i = 0; i < 20; i++) { c = thestack.pop(); System.out.println( Consumed: + c); try { Thread.sleep((int) (Math.random() * 1000)); catch (InterruptedException e) { // αγνοήστε την Αυτή θα συλλέξει 20 χαρακτήρες από τη στοίβα, με καθυστέρηση ανάμεσα σε κάθε προσπάθεια. Η καθυστέρηση θα είναι μεταξύ 0 και 2 δευτερολέπτων. Αυτό σημαίνει ότι η στοίβα θα αδειάζει πιο γρήγορα απ ό,τι θα γεμίζει και συνεπώς θα γεμίσει πλήρως πολύ σύντομα. Ας δούμε τώρα τη δόμηση της κλάσης της στοίβας. Χρειαζόμαστε ένα δείκτη (index) και ένα πίνακα ως ενδιάμεση μνήμη (buffer array). Η ενδιάμεση μνήμη δε θα πρέπει να είναι πολύ μεγάλη, καθώς σκοπός της άσκησης είναι να δείξουμε ότι υπάρχει ορθή λειτουργία και συγχρονισμός όταν γεμίζει. Στην περίπτωση αυτή ο πίνακας θα είναι 6 χαρακτήρων. 13.6.3. Η κλάση SyncStack Μία στιγμιότυπο της SyncStack που μόλις έχει φτιαχτεί θα πρέπει να είναι κενό, κάτι που μπορεί να επιτευχθεί εύκολα χρησιμοποιώντας την εξ ορισμού αρχικοποίηση των τιμών, αλλά για λόγους σωστής τακτικής θα παρουσιάσουμε τη διαδικασία αυτή ρητά. Συνεπώς ξεκινάμε να φτιάχνουμε την κλάση μας. class SyncStack { private int index = 0; private char [] buffer = new char[6]; 17

public synchronized char pop() { public synchronized void push(char c) { Παρατηρήστε την έλλειψη συναρτήσεων δημιουργίας. Θα ήταν ίσως καλύτερο για λόγους στυλ να τις παρουσιάσουμε, αλλά για λόγους συντομίας τις παραλείψαμε. Ας δούμε τώρα τις μεθόδους push και pop. Θα χρειαστεί να τις κάνουμε synchronized, για να προστατέψουμε τα ευαίσθητα στοιχεία δεδομένων index και buffer. Επιπρόσθετα, χρειάζεται να φροντίσουμε να γίνεται wait() αν η μέθοδος δεν μπορεί να συνεχίσει, και notify() όταν κάνουμε τη δουλειά μας. Η μέθοδος pop() είναι έτσι: public synchronized void push(char c) { while (index == buffer.length) { try { this.wait(); catch (InterruptedException e) { // αγνόησε το this.notify(); buffer[index] = c; index++; Παρατηρήστε ότι η κλήση στη wait() έγινε ρητά από ως this.wait(). Η χρήση του this είναι πλεονάζουσα, αλλά έχει συμπληρωθεί για να δοθεί έμφαση στο ότι το ραντεβού (rendezvous) γίνεται στο συγκεκριμένο (this) αντικείμενο στοίβας. Η κλήση wait() τοποθετείται σε μία δομή try/catch. Εξ αιτίας της δυνατότητας η wait() να βγει εξ αιτίας μίας interrupt() πρέπει να έχουμε επανάληψη στον έλεγχο για την περίπτωση που το Thread «ξύπνησε» από τη wait() πρόωρα. Έστω η κλήση στη notify(). Και πάλι έχουμε μία ρητή this.notify(), που είναι πλεονάζουσα, αλλά περιγραφική. Τι γίνεται με το σημείο που καλείται η notify(). Γίνεται προτού γίνει η αλλαγή, και γιατί δεν είναι λάθος; Η απάντηση είναι ότι οποιοδήποτε νήμα είχε σταματήσει εξ αιτίας του wait() δεν μπορεί να συνεχίσει προτού να βγούμε από το συγχρονισμένο μπλοκ, συνεπώς μπορούμε να δώσουμε τη notify() όποτε θέλουμε, αφού γνωρίζουμε ότι πρόκειται να συνεχίσουμε με τις αναγκαίες αλλαγές. Το τελικό σημείο είναι το ζήτημα του ελέγχου για λάθη. Μπορεί να παρατηρήσετε ότι δεν υπάρχει ρητός κώδικας για να αποτραπεί η κατάσταση υπερχείλισης. Αυτή δεν είναι απαραίτητη καθώς ο μόνος τρόπος για να προσθέσουμε κάτι στη στοίβα είναι μέσω αυτής της μεθόδου, και η μέθοδος αυτή θα μπει μέσα στο βρόχο του wait() αν κληθεί σε τέτοιες συνθήκες ώστε να προκαλέσει υπερχείλιση. Συνεπώς ο έλεγχος για λάθη δεν είναι απαραίτητος. Μπορούμε να είμαστε σίγουροι για αυτό σε ένα εκτελούμενο σύστημα για περισσότερους του ενός λόγους. Αν η λογική μας προκύψει ότι είναι προβληματική, θα έχουμε κάνει μία προσπέλαση πέρα από τα όρια του πίνακα, πράγμα που θα προκαλέσει άμεσα μία Exception, συνεπώς δεν υπάρχει πιθανότητα να μην παρατηρήσετε αυτό το 18

λάθος. Σε άλλες περιπτώσεις μπορείτε να χρησιμοποιήσετε εξαιρέσεις κατά το χρόνο εκτέλεσης για να τοποθετήσετε τους ελέγχους σας. public synchronized char pop() { while (index == 0) { try { this.wait(); catch (InterruptedException e) { // αγνοήστε την this.notify(); index--; return buffer[index]; Το μόνο που απομένει είναι να τοποθετήσουμε τα κομμάτια αυτά σε πλήρεις κλάσεις και να δώσουμε και τα στοιχεία για να τα βάλουμε να τρέξουν όλα μαζί. Ακολουθεί ο τελικός κώδικας. SyncTest.java package mod13; public class SyncTest { public static void main (String args[]) { SyncStack stack = new SyncStack(); Runnable source = new Producer(stack); Runnable sink = new Consumer(stack); Thread t1 = new Thread(source); Thread t2 = new Thread(sink); t1.start(); t2.start(); Producer.java package mod13; public class Producer implements Runnable { SyncStack thestack; public Producer(SyncStack a) { thestack = a; public void run() { char c; for (int i = 0; i < 20; i++) { c = (char) (Math.random() * 26 + A ); 19

thestack.push ; System.out.println( Produced: + c); try { Thread.sleep((int)(Math.random() * 100)); catch (InterruptedException e) { // αγνοήστε την Consumer.java package mod13; public class Consumer implements Runnable { SyncStack thestack; public Consumer (SyncStack a) { thestack = a; public void run() { char c; for (int i = 0; i < 20; i++) { c = thestack.pop(); System.out.println( Consumed: + c); try { Thread.sleep((int) (Math.random() * 1000)); catch (InterruptedException e) { // αγνοήστε την SyncStack.java package mod13; class SyncStack { private int index = 0; private char [] buffer = new char[6]; public synchronized char pop() { while (index == 0) { try { this.wait(); catch (InterruptedException e) { 20

// αγνοήστε την this.notify(); index--; return buffer[index]; public synchronized void push(char c) { while (index == buffer.length) { try { this.wait(); catch (InterruptedException e) { // αγνόησε το this.notify(); buffer[index] = c; index++; 13.7 Εργασίες 13.7.1. Επιπέδου 1: Τρία νήματα 1. Γράψτε ένα απλό πρόγραμμα που δημιουργεί τρία νήματα: κάθε ένα θα πρέπει να παρουσιάζει την ώρα που εκτελέστηκε (μπορείτε να χρησιμοποιήσετε την κλάση Date()). 21