Συντρέχων προγραμματισμός (Concurrent programming)

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

Το συντρέχον λογισμικό διακρίνεται σε λογισμικό συστήματος και λογισμικό εφαρμογών. Ουσιαστικά το λειτουργικό σύστημα είναι κατ' εξοχήν συντρέχον, βασιζόμενο στην έννοια της διεργασίας ή και του νήματος επιπέδου πυρήνα: ο πυρήνας του λειτουργικού συστήματος αντιμετωπίζει όλα τα εκτελούμενα προγράμματα ως συντρέχουσες διεργασίες προς διαχείριση (πχ δρομολόγηση). Το λογισμικό εφαρμογών αναλαμβάνει να βελτιστοποιήσει την συμπεριφορά μιας εφαρμογής με τη χρήση τεχνικών συντρέχοντος προγραμματισμού, κυρίως πολυνηματικό προγραμματισμό επιπέδου εφαρμογής, χωρίς αυτή η διαδικασία να είναι απαραίτητα πλήρως γνωστή στο λειτουργικό σύστημα. Καθώς οι υπολογιστές αποκτούν πολλαπλούς επεξεργαστές (ή πυρήνες-cores), η συντρέχουσα εκτέλεση γίνεται όλο και πιο σημαντική. Εδώ θα ασχοληθούμε με συντρέχοντα προγραμματισμό επιπέδου εφαρμογής.
Η Java σχεδιάστηκε εξ' αρχής για να παρέχει συντρέχουσα εκτέλεση σε επίπεδο γλώσσας και βιβλιοθηκών. Από την έκδοση 5.0 και μετά προσφέρει και APIs υψηλότερου επιπέδου στα πακέτα java.util.concurrent.

Διεργασίες (Processes) και Νήματα (Threads)

Στο συντρέχοντα προγραμματισμό υπάρχουν δυο βασικές μονάδες εκτέλεσης: οι διεργασίες (processes) και τα νήματα (threads). Στη Java χρησιμοποιούνται κυρίως τα νήματα, όμως και οι διεργασίες είναι σημαντικές. Κάθε στιγμιότυπο της JVM είναι εξ' ορισμού μια διεργασία. Τα νήματα μπορούν να υπάρξουν μόνο στα πλαίσια μιας διεργασίας, ή αλλοιώς μια διεργασία αποτελείται από τουλάχιστο ένα νήμα.
Ένας υπολογιστής διαχειρίζεται (εκτελεί) τα νήματα των διεργασιών. Αυτό ισχύει ακόμη σε υπολογιστές με ένα πυρήνα, που συνεπώς εκτελούν ένα μόνο νήμα κάθε φορά. Ο υπολογιστικός χρόνος για ένα πυρήνα κατανέμεται στα νήματα μέσω της υπηρεσίας του λειτουργικού συστήματος που ονομάζεται πολυπρογραμματισμός (multiprogramming, multitasking).

Διεργασίες

Μια διεργασία είναι ένα πλήρως αυτόνομο περιβάλλον εκτέλεσης. Μια διεργασία έχει στη διάθεσή της ένα πλήρες ιδιωτικό σύνολο πόρων, που τις έχουν εκχωρηθεί από το λειτουργικό σύστημα ώστε να μπορέσει να εκτελεστεί: χώρο διευθύνσεων, περιγραφείς αρχείων κλπ. Οι διεργασίες είναι πλήρως ορατές και διαχειρίσιμες από το λειτουργικό σύστημα.
Μια διεργασία συχνά για λόγους απλότητας θεωρείται συνώνυμη του στιγμιοτύπου ενός προγράμματος που εκτελείται. Στη πράξη βέβαια η εκτέλεση μιας εφαρμογής πιθανώς να απαιτεί τη συνεργασία πολλών διεργασιών. Η συνεργασία διεργασίών σε επίπεδο λειτουργικού συστήματος επιτυγχάνεται με τη Δια-Διεργασιακή Επικοινωνία  (Inter Process Communication - IPC). Κατ'επέκταση η IPC χρησιμοποιείται και για συνεργασία διεργασιών που βρίσκονται σε διαφορετικά δικτυωμένα συστήματα.
Οι περισσότερες υλοποιήσεις της JVM αντιμετωπίζονται από τα λειτουργικά συστήματα ως μια διεργασία. Μια εφαρμογή Java μπορεί να δημιουργήσει επιπλεόν διεργασίες με τη χρήση του αντικειμένου ProcessBuilder. Τέτοιες εφαρμογές δεν συζητούνται στο παρόν κείμενο.

Νήματα

Ένα νήμα υπάρχει μόνο μέσα σε μια διεργασία -ή αλλοιώς κάθε διεργασία έχει τουλάχιστο ένα νήμα. Επιπλέον, κάθε διεργασία έχει και έναν αριθμό νημάτων επιπέδου πυρήνα, που είναι υπεύθυνα για την επικοινωνία με το λειτουργικό σύστημα, όμως για τους σκοπούς μας δεν είναι ορατά στο χρήστη. Τα νήματα μοιράζονται τους πόρους της διεργασίας, για παράδειγμα χώρο διευθύνσεων και περιγραφείς αρχείων. Αυτό επιτρέπει εύκολη μεν αλλά προβληματική επικοινωνία μεταξύ τω νημάτων.
Στη Java κάθε εφαρμογή ξεκινά με ένα νήμα, το κύριο νήμα  (main thread), το οποίο έχει τη δυνατότητα δημιουργίας άλλων νημάτων. Κάθε νήμα σχετίζεται με ένα αντικείμενο της κλάσης Thread. Υπάρχουν δύο βασικές στρατηγικές χρήσης αντικειμένων Thread.
Η δεύτερη στρατηγική εξετάζεται παρακάτω. Αναφέρεται στην επίτευξη συνδρομικότηατς με χρήση έτοιμων δομών υψηλοτέρου επιέδου.

Δημιουργία και Εκκίνηση Νήματος

Η δημιουργία ενός στιγμιοτύπου της κλάσης Thread πρέπει να συνοδεύεται από το κώδικα που θα εκτελεί το νήμα. Αυτό μπορεί να γίνει με δύο τρόπους:
Και στα δύο παραδείγματα η εκκίνηση πραγματοποιείται με τη κλήση Thread.start.
Η πρώτη μέθοδος, του Runnable αντικειμένου, είναι γενικότερη, επειδή ένα the Runnable αντικείμενο μπορεί να επεκταθεί και με άλλες υποκλάσεις πέραν της Thread. Η δεύτερη μέθοδος είναι μεν χρήσιμη για απλές εφαρμογές, αλλά είναι περιοριστική αφού επιτρέπει επέκταση μόνο της κλάσης Thread. Εδώ χρησιμοποιούμε τη πρώτη μέθοδο, η οποία ξεχωρίζει τη Runnable εργασία από το αντικείμενο Thread που την εκτελεί. Αυτή η μέθοδος όχι μόνο είναι γενικότερη αλλά επιτρέπει και την εφαρμογή ΑPIs διαχείρισης νημάτων υψηλοτέρου επιπέδου.
Η κλάση Thread class ορίζει διάφορες μεθόδους. Μερικές static μέθοδοι παρέχουν πληροφορίες ή τροποποιούν τη κατάσταση του νήματος. Άλλες μέθοδοι σχετίζονται με τη διαχείριση του νήματος. Οι βασικότερες μέθοδοι εξετάζονται παρακάτω.

Αναστολή Εκτέλεσης με Sleep

Η μέθοδος Thread.sleep αναστέλει την εκτέλεση του νήματος για δεδομένο χρονικό διάστημα. Με αυτό το τρόπο ένα νήμα παραχωρεί χρόνο εκτέλεσης σε άλλα νήματα της εφαρμογής ή σε άλλες εφαρμογές. Η μέθοδος sleep μπορεί επίσης να χρησιμοποιηθεί για ρύθμιση της ταχύτητας εκτέλεσης, όπως φαίνεται στο παρακάτω παράδειγμα, ή για αναμονή εκτέλεσης κάποιου άλλου νήματος που πιθανόν να είναι πιο απαιτητικό σε χρόνο εκτέλεσης.
Το χρονικό διάστημα στη sleep ορίζεται σε milliseconds ή σε nanoseconds. Ο ακριβής χρονισμός εξαρτάται από την υλοποίηση της JVM και το λειτουργικό σύστημα. Επίσης η περίοδος αναστολής μπορεί να διακοπεί από κάποιο σήμα προς το νήμα, όπως θα δούμε αργότερα. Σε κάθε περίπτωση η διάρκεια εκτέλεσης της sleep είναι προσεγγιστική.
Το παράδειγμα SleepMessages χρησιμοποιεί τη sleep για την εμφάνιση μηνυμάτων ανά 4 sec.:
public class SleepMessages {
    public static void main(String args[]) throws InterruptedException {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        for (int i = 0; i < importantInfo.length; i++) {
            //Pause for 4 seconds
            Thread.sleep(4000);
            //Print a message
            System.out.println(importantInfo[i]);
        }
    }
}
Σημειώστε οτι στο main δηλώνεται throws InterruptedException. Αυτό σημαίνει οτι όταν ένα νήμα προσπαθήσει να διακόψει τη sleep η διακοπή αυτή απορρίπτεται. Αυτό συμβαίνει γιατί στην εφαρμογή μας δεν έχουμε ορίσει κάποιο άλλο νήμα που να προκαλεί διακοπή. Επομένως η εφαρμογή δεν ενδιαφέρεται να συλλάβει το InterruptedException.

Διακοπές (Interrupts)

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

Διαχείριση Διακοπής

Η διαχείριση διακοπής εξαρτάται από το τι κάνει το νήμα. Αν το νήμα εκτελεί μεθόδους που μπορούν να διακοπούν, δηλαδή προκαλούν InterruptedException, τότε η σύλληψη του InterruptedException απλά σημαίνει επιστροφή από τη μέθοδο run. Για παράδειγμα, έστω οτι ο κεντρικός βρόχος του παραδείγματος SleepMessages βρίσκεται στη μέθοδο run ενός Runnable αντικειμένου νήματος. Τότε η διαχείριση διακοπής θα είχε τη μορφή:
for (int i = 0; i < importantInfo.length; i++) {
    //Pause for 4 seconds
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        //We've been interrupted: no more messages.
        return;
    }
    //Print a message
    System.out.println(importantInfo[i]);
}
Αρκετές μέθοδοι, όπως η sleep, προκαλούν InterruptedException και είναι σχεδιασμένες ώστε να διακόπτουν την εκτέλεσή τους και να επιστρέφουν άμεσα μόλις λάβουν σήμα διακοπής.
Τι συμβαίνει αν το νήμα εκτελεί μια μέθοδο που δεν είναι σχεδιασμένη να επιστρέφει άμεσα σε σήμα διακοπής προκαλώντας InterruptedException; Τότε το νήμα πρέπει κατά περιόδους να καλεί τη μέθοδο Thread.interrupted, η οποία επιστρέφει true αν εν τω μεταξύ έχει παραληφθεί διακοπή. Για παράδειγμα:
for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        //We've been interrupted: no more crunching.
        return;
    }
}
Εδώ ο κώδικας απλά ελέγχει για σήμα διακοπής και επιστρέφει αν έχει ληφθεί τέτοιο σήμα. Σε ένα πιο σύνθετο παράδειγμα πιθανώς θα είχε νόημα η πρόκληση ενός InterruptedException:
if (Thread.interrupted()) {
    throw new InterruptedException();
}
το οποίο θα μπορούσε να το διαχειριστεί μια κεντρική πρόταση catch.

Σημαία Κατάστασης Διακοπών (Interrupt Status Flag)

Ο μηχανισμός διακοπών ενός νήματος υλοποιείται με τη βοήθεια μιας "εσωτερικής" σημαίας που ονομάζεται κατάσταση διακοπών (interrupt status). Η κλήση της Thread.interrupt δίνει τιμή στη σημαία κατάστασης του αντίστοιχου νήματος. Όταν το νήμα ελέγχει για διακοπή με τη κλήση της στατικής μεθόδου Thread.interrupted, η σημαία κατάστασής του καθαρίζεται. Η μη-στατική μέθοδος isInterrupted, που μπορεί να χρησιμοποιηθεί από ένα νήμα για να ελέγξει τη κατάσταση διακοπών ενός άλλου νήματος, δεν τροποποιεί τη τιμή της σημαίας.
Μια μέθοδος που τερματίζεται προκαλώντας ένα InterruptedException καθαρίζει τη σημαία κατάστασης. Όμως είναι πάντα πιθανό η κατάσταση να αλλάξει από κάποιο άλλο νήμα που θα καλέσει την interrupt.

Δαίμονες

Με τη δήλωσηη setDaemon(true) στο κατασκευαστή του νήματος ένα νήμα μπορεί να γίνει δαίμονας. Στα απλά νήματα η JVM περιμένει το τερματισμό των νημάτων για να τερματίσει τη λειτουργία της. Στους δαίμονες αυτό δεν ισχύει. Μόλις τερματίσει το τελευταίο απλό νήμα, η JVM τερματίζεται ακόμη και αν υπάρχουν νήματα δαίμονες που συνεχίζουν.
public class DaemonTest {

    public static void main(String[] args) {
        new WorkerThread().start();
        try {
            Thread.sleep(7500);
        } catch (InterruptedException e) {}
        System.out.println("Main Thread ending") ;
    }

}
class WorkerThread extends Thread {

    public WorkerThread() {
        setDaemon(true) ;   // When false, (i.e. when it's a user thread),
                // the Worker thread continues to run.
                // When true, (i.e. when it's a daemon thread),
                // the Worker thread terminates when the main 
                // thread terminates.
    }

    public void run() {
        int count=0 ;
        while (true) {
            System.out.println("Hello from Worker "+count++) ;
            try {
                sleep(5000);
            } catch (InterruptedException e) {}
        }
    }
}

Ένωση (Join)

Η μέθοδος join αναγκάζει ένα νήμα να περιμένει το τερματισμό άλλου νήματος. Έστω t ένα αντικείμενο Thread που εκτελείται συνδρομικά με ένα άλλο νήμα. Αν το δεύτερο νήμα φτάσει στη κλήση
t.join();
τότε το δεύτερο νήμα θα αναστείλει την εκτέλεσή του μέχρι το νήμα t να τερματίσει. Εκδοχές της join επιτρέπουν τον καθορισμό χρόνου αναμονής. Όμως, όπως και στη sleep, ο χρονισμός δεν είναι απόλυτα ακριβής.
Η join, όπως και η sleep, όταν δέχεται διακοπή τερματίζει με InterruptedException.

Απλό Παράδειγμα

Το ακόλουθο παράδειγμα συνοψίζει μερικές από τις προηγούμενες έννοιες. Η κλάση SimpleThreads ξεκινά, όπως όλες οι εφαρμογές Java, με το κύριο νήμα. Το κύριο νήμα δημιουργεί ένα νήμα από ένα Runnable αντικείμενο, το MessageLoop, και περιμένει το τερματισμό του νήματος. Αν το νήμα MessageLoop αργήσει πολύ να τερματίσει, το κύριο νήμα το διακόπτει.
Το νήμα MessageLoop εμφανίζει διάφορα μηνύματα. Αν διακοπεί πριν τερματίσει εμφανίζει ένα μήνυμα διαμαρτυρίας και τερματίζει.
public class SimpleThreads {
    //Display a message, preceded by the name of the current thread
    static void threadMessage(String message) {
        String threadName = Thread.currentThread().getName();
        System.out.format("%s: %s%n", threadName, message);
    }
    private static class MessageLoop implements Runnable {
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (int i = 0; i < importantInfo.length; i++) {
                    //Pause for 4 seconds
                    Thread.sleep(4000);
                    //Print a message
                    threadMessage(importantInfo[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }
    public static void main(String args[]) throws InterruptedException {
        //Delay, in milliseconds before we interrupt MessageLoop
        //thread (default one hour).
        long patience = 1000 * 60 * 60;
        //If command line argument present, gives patience in seconds.
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("Argument must be an integer.");
                System.exit(1);
            }
        }
        threadMessage("Starting MessageLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();
        threadMessage("Waiting for MessageLoop thread to finish");
        //loop until MessageLoop thread exits
        while (t.isAlive()) {
            threadMessage("Still waiting...");
            //Wait maximum of 1 second for MessageLoop thread to
            //finish.
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience) &&
                    t.isAlive()) {
                threadMessage("Tired of waiting!");
                t.interrupt();
                //Shouldn't be long now -- wait indefinitely
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}

Επικοινωνία και Συγχρονισμός

Τα νήματα επικοινωνούν κυρίως μέσω κοινόχρηστων (μοιραζόμενων) περιοχών στο χώρο διευθύνσεων. Αυτή η μέθοδος επικοινωνίας είναι εξαιρετικά αποδοτική αλλά επιτρέπει δύο τύπους σφαλμάτων: συνθήκες ανταγωνισμού (race conditions) και συνθήκες προήγησης (precedence conditions). Τα σφάλματα αποτρέπονται μέσω του συγχρονισμού (synchronization). O συγχρονισμός επιτυχγάνεται με ειδικά εργαλεία και μεθόδους που περιγράφονται παρακάτω.
Ωστόσο ο συγχρονισμός μπορεί να δημιουργήσει ανταγωνισμό μεταξύ των νημάτων, ο οποίος εμφανίζεται όταν δύο ή περισσότερα νήματα προσπαθούν συνδρομικά να αποκτήσουν πρόσβαση στις κοινές περιοχές μνήμης και το αναγκάζουν την Java να εκτελεί ένα ή περισσότερα νήματα πιο αργά είτε ακόμη και να αναστέλλει την εκτέλεσή τους. Το αποτέλεσμα αυτού του ανταγωνισμοού για διαμοιραζόμενες περιοχές μνήμης παίζει σημαντικό ρόλο στην ζωτικότητα (liveness) των νημάτων που οδηγεί μερικές φορές στην λιμοκτονία (starvartion) τους.

Συνθήκες Ανταγωνισμού

Έστω μια απλή κλάση Counter
class Counter {
    private int c = 0;
    public void increment() {
        c++;
    }
    public void decrement() {
        c--;
    }
    public int value() {
        return c;
    }
}
Η κλάση Counter έχει σχεδιαστεί ώστε κάθε κλήση της μεθόδου increment αυξάνει την μεταβλητή c κατά 1, ενώ κάθε κλήση της decrement τη μειώνει κατά 1. Όμως, αν ένα αντικείμενο Counter μοιράζεται μεταξύ πολλών νημάτων, οι συνδρομικές τροποποιήσεις της μοιραζόμενης μεταβλητής  μπορεί να οδηγήσει σε σφάλματα.
Για να γίνει αντιληπτό το πιθανό σφάλμα πρέπει να ληφθεί υπ' όψη οτι οι εκφράσεις c++ και c-- αναλύονται σε επιμέρους βήματα γλώσσας εικονικής μηχανής ως εξής:
  1. Ανάκληση της τρέχουσας τιμής της c.
  2. Αύξηση (ή Μείωση) της ανακληθείσας τιμής κατά 1.
  3. Αποθήκευση της νέας τιμής της c.
Έστω οτι ένα νήμα A καλεί την increment και περίπου την ίδια στιγμή το νήμα B καλεί την decrement. Έστω επίσης οτι η αρχική τιμή της c είναι 0, και τα νήματα εκτελούν τα επιμέρους βήματα με τη παρακάτω σειρά:
  1. A: Ανάκληση της τρέχουσας τιμής της c. (0)
  2. B: Ανάκληση της τρέχουσας τιμής της c. (0)
  3. A: Αύξηση της ανακληθείσας τιμής κατά 1. (1)
  4. B: Μείωση της ανακληθείσας τιμής κατά 1. (-1).
  5. A: Αποθήκευση της νέας τιμής της c (1).
  6. B: Αποθήκευση της νέας τιμής της c (-1).
Το αποτέλεσμα του νήματος Α έχει υπερ-γραφεί από αυτό του νήματος Β.

Συνθήκες Προήγησης

Οι συνθήκες προήγησης προκύπτουν όταν οι λειτουργίες που εκτελούν δύο ή περισσότερα νήματα συνδέονται με μια σχέση τύπου συμβαίνει-πριν (happens-before). Η σχέση αυτή σημαίνει οτι αν οι λειτουργίες εκτελούνταν από ένα νήμα, δηλαδή όχι συνδρομικά, τότε θα είχαν μια συγκεκριμένη σειρά εκτέλεσης, η οποία πρέπει να διασφαλιστεί και στη συνδρομική εκτέλεση. Έστω το παρακάτω παράδειγμα:

int counter = 0;

Η μεταβλητή counter διαμοιράζεται σε δύο νήματα, A και B. Έστω οτι το νήμα A αυξάνει τη μεταβλητή counter:

counter++;

Πολύ λίγο αργότερα (σχεδόν ταυτόχρονα) το νήμα B εμφανίζει τη τιμή της μεταβλητής counter:

System.out.println(counter);

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

Δύο περιπτώσεις σχέσης συμβαίνει-πριν που έχουμε ήδη συναντήσει:

        Ένας πλήρης κατάλογος των συνθηκών προήγησης στη Java δίνεται στο πακέτο java.util.concurrent.

Συγχρονισμένες Μέθοδοι

Η Java παρέχει δύο επίπεδα συγχρονισμού: συγχρονισμένες μεθόδους και συχγρονισμένες εντολές. Πρώτα συζητούνται οι συγχρονισμένες μέθοδοι.
Για να καταστήσουμε μια μέθοδο συγχρονισμένη απλά προσθέτουμε στη δήλωσή της τη λέξη-κλειδί synchronized:
public class SynchronizedCounter {
    private int c = 0;
    public synchronized void increment() {
        c++;
    }
    public synchronized void decrement() {
        c--;
    }
    public synchronized int value() {
        return c;
    }
}
  Έστω count ένα αντικείμενο της κλάσης SynchronizedCounter, τότε το γεγονός οτι οι μέθοδοι του αντικειμένου είναι συγχρονισμένες έχει τις εξής συνέπειες:
Σημειώστε οτι οι κατασκευαστές δεν μπορούν να συχγρονιστούν — η λέξη-κλειδί synchronized προκαλεί συντακτικό σφάλμα. Ο συχγρονισμός κατασκευαστών δεν έχει νόημα αφού μόνο ένα νήμα μπορεί να κατασκευάσει ένα αντικείμενο κάθε φορά.
Οι συγχρονισμένες μέθοδοι παρέχουν ένα απλό εργαλείο αποφυγής προβλημάτων επικοινωνίας και συγχρονισμού: αν πολλά νήματα έχουν πρόσβαση σε ένα αντικείμενο τότε όλες οι μέθοδοι που αναφέρονται στο αντικείμενο (με εξαίρεση τον κατασκευαστή) πρέπει να είναι synchronized. Η στρατηγική αυτή είναι αποτελεσματική αλλά η μπορεί να οδηγήσει σε αδιέξοδο. 

Κλειδώματα (Locks) και Συγχρονισμένες Εντολές

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

Κλειδώματα και Συγχρονισμένες Μέθοδοι

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

Συγχρονισμένες Εντολές

Η δεύτερη μέθοδος συγχρονισμού είναι οι συγχρονισμένες εντολές. Στις συγχρονισμένες εντολές έχουμε δύο εκδοχές, Στη πρώτη εκδοχή συγχρονίζουμε τις λειτουργίες σε ένα αντικείμεο. Το αντικείμενο εφαρμογής πρέπει να δηλώνεται ρητά, ώστε να καταληφθεί το αντίστοιχο κλείδωμα:
public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}
Στο παραπάνω παράδειγμα η μέθοδος addName πρέπει να συγχρονίσει τις μεταβολές στα πεδία lastName και nameCount ενός αντικειμένου. Εναλλακτικά θα έπρεπε να συγχρονίσουμε τη μέθοδο nameList.add.
Στη δεύτερη εκδοχή, συγχρονίζουμε συγκεκριμένες μόνο εντολές. Ο βασικός λόγος χρήσης των συχγρονισμένων εντολών είναι η βελτίωση της συντρέχοντητας μέσω λεπτομερούς συγχρονισμού. Έστω, για παράδειγμα, η κλάση MsLunch έχει δύο μεταβλητές, c1 και c2, που ποτέ δε χρησιμοποιούνται μαζί. Οι ενημερώσεις της κάθε μεταβλητής θα πρέπει να συγχρονίζονται αλλά δεν υπάρχει λόγος συνολικού συγχρονισμού των ενημερώσεων. Ο συνολικός συγχρονισμός θα προκαλούσε αναίτιες καθυστερήσεις. Μπορούμε, αντί να συγχρονίσουμε μεθόδους ή αντικείμενα με χρήση του this, να δημιουργήσουμε δικά μας κλειδώματα.
public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }
    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}
Η χρήση συγχρονισμένων αντικειμένων ή εκφράσεων απαιτεί ιδιαίτερη προσοχή. Πρέπει να είμαστε σίγουροι οτι η κατάληψη ενός κλειδώματος δεν προκαλεί προβλήματα (πχ αδιέξοδα) σε άλλα νήματα.

Επανεισαγόμενος Συγροχονισμός

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

Ατομικές Λειτουργίες (Atomic Operations)

Στο προγραμματισμό, ατομικές (μη-διακοπτόμενες) λέγονται οι λειτουργίες που δεν μπορούν να διακοπούν: είτε δεν εκτελούνται ή εκτελούνται πλήρως.
Φαινομενικά απλές εκφάσεις, όπως η c++, δεν είναι ατομικές, αφού μπορούν να αποσυντεθούν σε απλούστερες λειτουργίες, όπως είδαμε. 'Ομως ορισμένες λειτουργίες είναι πάντα ατομικές, όπως για παράδειγμα η ανάγνωση / εγγραφή της τιμής μιας μεταβλητής (εκτός από long και double). Στη περίπτωση που μια μεταβλητή έχει δηλωθεί volatile, τότε ανεξάρτητα από το τύπο της μεταβλητής, η ανάγνωση / εγγραφή είναι ατομική λειτουργία. Οι ατομικές λειτουργίες διασφαλίζουν οτι δεν θα υπάρξει συνθήκη ανταγωνισμού. Όμως παραμένει η ανάγκη συγχρονισμού αφού δεν διασφαλίζουν οτι δεν θα υπάρξει συνθήκη προήγησης.
Η χρήση μεταβλητών τύπου volatile μειώνει τις πιθανότητες να υπάρξει συνθήκη προήγησης επειδή κάθε εγγραφή σε μεταβλητή τύπου volatile καθορίζει μια σχέση συμβαίνει-πριν με τις επόμενες αναγνώσεις της ίδια μεταβλητής. Αυτό σημαίνει οτι οι τροποποιήσεις μιας μεταβλητής τύπου volatile είναι πάντα ορατές σο όλα τα νήματα.

Ζωτικότητα (Liveness)

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

Αδιέξοδο (Deadlock)

Αδιέξοδο ονομάζεται η κατάσταση που δύο ή περισσότερα νήματα αναστέλλονται, καθώς το ένα προσπαθεί να καταλάβει ένα κλείδωμα που κατέχει άλλο νήμα, και αντίστροφα, έτσι ώστε δημιουργείται ένας κύκλος, ενώ κανένα νήμα δεν έχει τρόπο να διασπάσει αυτό το κύκλο.
Παρακάτω, στο κώδικα Deadlock, βλέπουμε ένα παράδειγμα δυο πολύ ευγενικών νημάτων, των Alfonse και Gaston, που το ένα υποκλίνεται στο άλλο.
public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s has bowed to me!%n", 
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s has bowed back to me!%n",
                    this.name, bower.getName());
        }
    }
    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}
Όταν εκτελείται το πρόγραμμα Deadlock, τα δύο νήματα για να εκτελέσουν τη μέθοδο bow καταλαμβάνουν τα κλειδώματα των συγχρονισμένων μεθόδων που αναφέρονται στα (ξεχωριστά) αντικείμενά τους. Κατόπιν, το κάθε νήμα χωρίς να απελευθερώσει το κλειδώμά του, προσπαθεί να καταλάβει το κλείδωμα των συχρονισμένων μεθόδων του άλλου  νήματος για να εκτελέσει τη μέθοδο bowBack. Αυτό θα προκαλέσει αδιέξοδο, αφού τα κλειδώματα έχουν ήδη καταληφθεί.< face="monospace">

Φρουρούμενα Τμήματα (Guarded Blocks): Αναμονή και Ειδοποίηση

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

Έστω, για παράδειγμα, οτι η μέθοδος guardedJoy για να προχωρήσει πρέπει να ελέγξει τη τιμή της μοιραζόμενης μεταβλητής joy, η οποία τροποποιείται από άλλο νήμα. Η απλούστερη προσέγγιση είναι η ενεργός αναμονή (busy waiting) με τη μέθοδο να εκτελεί ένα βρόχο while σε αναμονή τροποποίησης της τιμής της μεταβλητής. Η προσέγγιση αυτή είναι αποδεκτή αν γνωρίζουμε οτι η αναμονή είναι σύντομη. Αν όμως η διάρκεια της αναμονής είναι μεγάλη ή απρόβλεπτη τότε η ενεργός αναμονή σπαταλά χρόνο μηχανής.

public void guardedJoy() {
    //Simple loop guard. Wastes processor time. Don't do this!
    while(!joy) {}
    System.out.println("Joy has been achieved!");
}

Μια πιο αποδοτική αναμονή επιτυγχάνεται με τη κλήση της μεθόδου Object.wait η οποία υποκαθιστά την ενεργό αναμονή με αναστολή. Η κλήση της wait δεν επιστρέφει μέχρις ότου ένα άλλο νήμα να ειδοποιήσει (να στείλει σήμα) οτι το αναμενόμενο γεγονός έχει συμβεί:

public synchronized guardedJoy() {
    //This guard only loops once for each special event, which may not
    //be the event we're waiting for.
    while(!joy) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}

Προσοχή: Πάντα η κλήση της wait πρέπει να γίνεται μέσα σε βρόχο while ο οποίος ελέχγει το αναμενόμενο γεγονός.  Ένα σήμα διακοπής μπορεί να έχει προκληθεί από άλλο γεγονός και όχι απο το αναμενόμενο..

Όπως αρκετές μέθοδοι που αναστέλουν την εκτέλεση, η wait μπορεί να απορρίψει τη InterruptedException. Στο παράδειγμα, μπορούμε απλά να αγνοήσουμε την διακοπ και να ασχοληθούμε με τη τιμή της μεταβλητής joy.

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

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

public synchronized notifyJoy() {
    joy = true;
    notifyAll();
}

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


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

Παραγωγός - Καταναλωτής

Θα χρησιμοποιήσουμε το σχήμα αναμονής και ειδοποίησης για την υλοποίηση μιας εφαρμογής Παραγωγού-Καταναλωτή (Producer-Consumer). Μια εφαρμογή τέτοιου τύπου διαμοιράζει δεδομένα μεταξύ δύο νημάτων: τον παραγωγό, που δημιουργεί δεδομένα, και τον καταναλωτή, που τα επεξεργάζεται. Ο συντονισμός είναι ουσιαστικός: Ο καταναλωτής δεν πρέπει να προσπαθεί να επεξεργαστεί δεδομένα πριν αυτά παραχθούν από τον παραγωγό. Ο παραγωγός πάλι δεν πρέπει να παράγει υπερβολικά πολλά δεδομένα πριν ο καταναλωτής τα επεξεργαστεί.

Στο παράδειγμα, τα δεδομένα είναι μια σειρά από μηνύματα, που διαμοιράζονται μέσω ενός αντικειμένου της κλάσης Drop:

public class Drop {
    //Message sent from producer to consumer.
    private String message;
    //True if consumer should wait for producer to send message, false
    //if producer should wait for consumer to retrieve message.
    private boolean empty = true;
    public synchronized String take() {
        //Wait until message is available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        //Toggle status.
        empty = true;
        //Notify producer that status has changed.
        notifyAll();
        return message;
    }
    public synchronized void put(String message) {
        //Wait until message has been retrieved.
        while (!empty) {
            try { 
                wait();
            } catch (InterruptedException e) {}
        }
        //Toggle status.
        empty = false;
        //Store message.
        this.message = message;
        //Notify consumer that status has changed.
        notifyAll();
    }
}

Το νήμα παραγωγού, που ορίζεται στη κλάση Producer, παράγει μια σειρά μηνύματα. Το string "DONE" υποδηλώνει οτι όλα τα μηνύματα έχουν αποσταλεί. Για να επιτύχουμε μια 'πραγματική' συμπεριφορά, ο παραγωγός αναστέλει την εκτέλεσή του για τυχαία χρονικά διαστήματα μεταξύ διαδοχικών αποστολών.

     import java.util.Random;
     public class Producer implements Runnable {
         private Drop drop;
         public Producer(Drop drop) {
             this.drop = drop;
         }
         public void run() {
             String importantInfo[] = {
                   "Mares eat oats",
                   "Does eat oats",
                   "Little lambs eat ivy",
                   "A kid will eat ivy too"
             };
             Random random = new Random();
             for (int i = 0; i < importantInfo.length; i++) {
                 drop.put(importantInfo[i]);
                 try {
                     Thread.sleep(random.nextInt(5000));
                 } catch (InterruptedException e) {}
             }
             drop.put("DONE");
         }
    }

Το νήμα καταναλωτή, που ορίζεται στο Consumer, λαμβάνει τα μηνύματα και τα εμφανίζει, μέχρι να παραλάβει το string "DONE". Και αυτό το νήμα σταματά για τυχαία χρονικά διαστήματα.

    import java.util.Random;
    public class Consumer implements Runnable {
         private Drop drop;
         public Consumer(Drop drop) {
             this.drop = drop;
         }
         public void run() {
            Random random = new Random();
            for (String message = drop.take(); ! message.equals("DONE");
                    message = drop.take()) {
                System.out.format("MESSAGE RECEIVED: %s%n", message);
                try {
                    Thread.sleep(random.nextInt(5000));

Αδιέξοδο (Deadlock)

Εν τέλει, το κύριο νήμα ορίζεται στο ProducerConsumerExample, απλά εκκινεί τα νήματα του παραγωγού και του καταναλωτή.

public class ProducerConsumerExample {
    public static void main(String[] args) {
        Drop drop = new Drop();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

Ενεργό Αδιέξοδο (Livelock)

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

Λιμοκτονία (Starvation)

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

Αμετάβλητα Αντικείμενα (Immutable Objects)

Ένα αντικείμενο θεωρείται αμετάβλητο (< face="DejaVu Sans Mono, monospace">< size="2">immutable) εάν η κατάστασή του δεν μεταβάλεται αφότου έχει κατασκευαστεί.  Αυτού του είδους τα αντικείμενα είναι ευρέως αποδεκτά από τους προγραμματιστές διότι έτσι δημιουργούν απλό και συνεπή κώδικα.
Τα αμετάβλητα αντικείμενα είναι χρήσιμα σε συνδρομικές εφαρμογές, επειδή δεν μεταβάλουν την κατάστασή τους, συνεπώς δεν μπορούν να μεταβληθούν από συνθήκες ανταγωνισμού μεταξύ πολλαπλών νημάτων.

Ένα παράδειγμα συγχρονισμένης κλάσης

Η κλάση SynchronizedRGB, ορίζει αντικείμενα που αντιστοιχούν σε χρώματα. Κάθε αντικείμενο αντιπροσωπεύει το χρώμα με τρεις ακεραίους που αναπαριστούν τις βασικές τιμές του χρώματος και ένα αλφαριθμητικό που είναι το όνομα του χρώματος.
public class SynchronizedRGB {

    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of " + name;
    }
}
H κλάση SynchronizedRGB πρέπει να χρησιμοποιηθεί προσεκτικά ώστε να μη βρεθεί σε ασυνεπή κατάσταση. Έστω, για παράδειγμα, οτι ένα νήμα εκτελεί το παρακάτω κώδικα:
SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      //Statement 1
String myColorName = color.getName(); //Statement 2
Αν ένα άλλο νήμα καλέσει τη color.set μετά τη πρόταση 1 αλλά πριν τη πρόταση 2, η τιμή myColorInt δε θα ταιριπάζει με τη τιμή myColorName. Για να αποφευχθεί αυτό,οι δύο προτάσεις πρέπει να εκτελεστούν αδιάσπαστα:
synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
}
Αυτού του είδους η ασυνέπεια εμφανίζεται σε μεταβαλλόμενα (mutable) αντικείμενα – αλλά δεν θα υπάρχει πρόβλημα στην αμετάβλητη (immutable) έκδοση του SynchronizedRGB.

Στρατηγικές δημιουργίας αμετάβλητων αντικειμένων

Οι παρακάτω οδηγίες μας δείχνουν τον τρόπο για την δημιουργία αμετάβλητων αντικειμένων.
  1. Μην παρέχετε μεθόδους καθορισμού τιμών (setter) - δηλαδή μεθόδους που μεταβάλουν πεδία ή αντικείμενα αναφέρονται μέσω πεδίων.
  2. Μεταβάλετε όλα τα πεδία σε final και private
  3. Μην επιτρέπετε υποκλάσεις να υπερκαλύπτουν (override) μεθόδους. Ο πιο απλός τρόπος είναι να μεταβάλουμε την κλάση σε final. Ένας πιο προχωρημένος τρόπος είναι να κάνουμε την κατασκευαστή της κλάσης private και να κατασκευάσουμε στιγμιότυπα αυτής της κλάσης σε εργοστασιακές (factory) μεθόδους.
  4. Εάν πεδία από στιγμιότυπα κλάσεων περιλαμβάνουν αναφορές σε μεταβαλλόμενα αντικείμενα, μην επιτρέπετε αυτά τα αντικείμενα να μεταβληθούν
Εφαρμόζοντας αυτές τις οδηγίες στην κλάση SynchronizedRGB θα πρέπει να ακολουθήσουμε τα παρακάτω βήματα:
  1. Υπάρχουν δύο setter μέθοδοι στην κλάση. Η πρώτη θέτει αυθαίρετους μετασχηματισμούς στο αντικείμενο και δεν έχει καμία θέση σε ένα αμετάβλητο (immutable) αντικείμενο. Το δεύτερο αντιστρέφει τις ακέραιες τιμές των χρωμάτων και μπορεί να προσαρμοστεί με το να επιστρέφει ένα νέο αντικείμενο από το να μεταβάλει το ήδη υπάρχων.
  2. Όλα τα πεδία είναι ήδη private, επίσης θα γίνουν και final.
  3. Η κλάση και αυτή θα γίνει final.
  4. Μόνο μια μέθοδος ( getRGB() ) αναφέρεται σε ένα αντικείμενο, και αυτό το αντικείμενο θα είναι αμετάβλητο (immutable). Για αυτό το λόγο, δεν χρειάζεται η χρήση της λέξη-κλειδί synchronized.
Μετά από τις αλλαγές, έχουμε την νέα κλάση ImmutableRGB:
final public class ImmutableRGB {

    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }


    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }

    public ImmutableRGB invert() {
        return new ImmutableRGB(255 - red,
                       255 - green,
                       255 - blue,
                       "Inverse of " + name);
    }
}

Συντρέχοντα Αντικείμενα Υψηλού Επιπέδου

Μέχρι τώρα έχουμε ασχοληθεί με το χαμηλού επιπέδου APIs τα οποία είναι ενσωματωμένο από την αρχή στην πλατφόρμα της Java. Αυτά τα APIs είναι επάρκή για βασικές λειτουργίες, αλλά για την ανάπτυξη μεγαλύτερων εφαρμογών απαιτούνται προγραμματιστικές δομές υψηλοτέρου επιπέδου.
Σε αυτό το τμήμα θα ασχοληθούμε με μερικές συντρέχουσες δομές υψηλού επιπέδου της πλατφόρμας Java. Τα περισσότερα από αυτά τα χαρακτηριστικά ενσωματώνονται στο πακέτο java.util.concurrent και στο Java Collection Framework.

Κλειδώματα υπό Συνθήκη (Condition)

Το πακέτο java.util.concurrent.locks παρέχει μεταξύ των άλλων ένα εξελιγμένο αντικείμενο Lock. Αυτά τα αντικείμενα Lock, όπως και τα απλά κλειδώματα, μπορούν να κατέχονται μόνο από ένα νήμα κάθε στιγμή. Όμως, επιπλέον υποστηρίζουν το μηχανισμό αναμονή και ειδοποίηση wait/notify, μέσω αντικειμένων Συνθήκης (αντικείμενα Condition).
Το μεγαλύτερο πλεονέκτημα των αντικειμένων Lock σε σχέση με τα απλά κλειδώματα είναι οτι παρέχουν μεθόδους που έχουν τη δυνατότητα να απεμπλέκονται από μια προσπάθεια κατάληψης ενός κλειδώματος. Η μέθοδος tryLock εγκαταλείπει τη προσπάθεια κατάληψης ενός κλειδώματος εάν αυτό είναι κατηλειμμένο ή αν περάσει ένα προκαθορισμένο χρονικό διάστημα (χρονοδιακοπή). Η μέθοδος lockInterruptibly εγκαταλείπει τη προσπάθεια κατάληψης ενός κλειδώματος εάν ένα άλλο νήμα στείλει σήμα διακοπής.
Θα εφαρμόσουμε τα αντικείμενα Lock για την επίλυση του προβλήματος του Αδιεξόδου. Τα δύο ευγενικά νήματα, Alphonse και Gaston, εκπαιδεύτηκαν ώστε να αναγνωρίζουν πότε το άλλο νήμα είνα έτοιμο να υποκλιθεί. Αυτή η βελτίωση μοντελοποιείται ως εξής: ένα αντικείμενο Friend για να προχωρήσει στη υπόκλιση πρέπει να έχει καταλάβει τα κλειδώματα και των δύο αντικειμένων. Αν καταλάβει μόνο το ένα κλείδωμα αλλά αποτύχει στη κατάληψη του δευτέρου τότε εγκαταλείπει προσωρινά και το πρώτο κλείδωμα, ώστε το άλλο νήμα να έχει την ευκαιρία μιας διπλής κατάληψης. Ο κώδικας δίενται στο πρόγραμμα Safelock. Τα δύο νήματα προσπαθούν διαρκώς να υποκλιθούν το ένα στο άλλο, κάνοντας ενδιάμεσα κάποια διαλείμματα τυχαίας χρονικς διάρκειας (το νήμα εκτελεί τη μέθοδο BowLoop). Η υπόκλιση (bow) ξεκινά με μια προσπάθεια κατάληψης των δύο κλειδωμάτων (impedingBow). Αν αυτή δεν επιτευχθεί τότε ελευθερώνεται και ο ένα κλείδωμα που πιθανώς είχε καταληφθεί. Σε περίπτωση επιτυχούς κατάληψης δύο κλειδωμάτων, παράγονται τα κατάλληλα μηνύματα και ελευθερώνονται τα κλειδώματα. Σε περίπτωση ανεπιτυχούς κατάληψης παράγεται μήνυμα που δηλώενι τη σύγκρουση και επαναλμβάνεται η προσπάθεια. Η μείωση της διάρκειας του διαλείμματος προφανώς αυξάνει τις πιθανότητες συγκρούσεων.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
public class Safelock {
    static class Friend {
        private final String name;
        private final Lock lock = new ReentrantLock();
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public boolean impendingBow(Friend bower) {
            Boolean myLock = false;
            Boolean yourLock = false;
            try {
                myLock = lock.tryLock();
                yourLock = bower.lock.tryLock();
            } finally {
                if (! (myLock && yourLock)) {
                    if (myLock) {
                        lock.unlock();
                    }
                    if (yourLock) {
                        bower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }
            
        public void bow(Friend bower) {
            if (impendingBow(bower)) {
                try {
                    System.out.format("%s: %s has bowed to me!%n", 
                            this.name, bower.getName());
                    bower.bowBack(this);
                } finally {
                    lock.unlock();
                    bower.lock.unlock();
                }
            } else {
                System.out.format("%s: %s started to bow to me, but" +
                        " saw that I was already bowing to him.%n",
                        this.name, bower.getName());
            }
        }
        public void bowBack(Friend bower) {
            System.out.format("%s: %s has bowed back to me!%n",
                    this.name, bower.getName());
        }
    }
    static class BowLoop implements Runnable {
        private Friend bower;
        private Friend bowee;
        public BowLoop(Friend bower, Friend bowee) {
            this.bower = bower;
            this.bowee = bowee;
        }
    
        public void run() {
            Random random = new Random();
            for (;;) {
                try {
                    Thread.sleep(random.nextInt(10));zenwalk linux
                } catch (InterruptedException e) {}
                bowee.bow(bower);
            }
        }
    }
            
    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(new BowLoop(alphonse, gaston)).start();
        new Thread(new BowLoop(gaston, alphonse)).start();
    }
}

Εκτελεστές (Executors)

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

Διασυνδέσεις Εκτελεστή (Executor Interfaces)

Το πακέτο java.util.concurrent ορίζει τρεις διασυνδέσεις για τα αντικείμενα εκτελεστών:
Τυπικά, οι τύποι των μεταβλητών που αναφέρονται σε αντικείμενα εκτελεστή, δηλώνονται με βάση τους τρεις παραπάνω τύπους διασύνδεσης.

Η διασύνδεση Εκτελεστή (Executor Interface)

Η διασύνδεση Executor παρέχει μια μέθοδο, execute, η οποία είναι έτσι σχεδιασμένη ώστε να αντικαθιστά την διασύνδεση του χρήστη με αντικείμενα τύπου Thread. Εάν r ένα Runnable αντικείμενο και e ένα Executor αντικείμενο αντικαθιστώντας Thread με Executor ως εξής:
        (new Thread(r)).start();
με
        e.execute(r);
Ωστόσο, ο ορισμός του αντικειμένου execute είναι γενικευμένος. Συγκεκριμένα ο χαμηλού επιπέδου κώδικας (της Thread) απλώς δημιουργεί ένα νέο νήμα και το εκκινεί αμέσως. Τώρα το νήμα εξαρτάται από τον εκτελεστή, και το συγκεκριμένο αντικείμενο θα κάνει ομοίως το ίδιο, δηλαδή θα εκκινήσει ένα νήμα, αλλά είναι επίσης πιθανό να χρησιμοποιήσει ένα υπάρχον νήμα από μία δεξαμενή νημάτων (thread pool) το οποίο μπορεί να χρησιμοποιηθεί για κάποια άλλη εργασία όταν θα είναι παλι διαθέσιμο.
Η υλοποίηση της διασύνδεσης executor γίνεται από το πακέτο java.util.concurrent, ώστε επίσης να έχει την δυνατότητα για πλήρη χρήση των διασυνδέσεων advancedExecutorService και ScheduledExecutorService.

Η διασύνδεση ExecutorService

Η διασύνδεση ExecutorService εμπλουτίζει την διασύνδεση Execute με μία παρόμοια αλλά πιο ευέλικτη μέθοδο submit. Όπως και στην διασύνδεση Execute, η μέθοδος submit δέχεται αντικείμενα Runnable αλλά και Callable, δηλαδή επιτρέπει στις διάφορες εργασίες (tasks) να επιστρέφουν μια τιμή. Η μέθοδος submit επιστρέφει ένα αντικείμενο Future, το οποίο χρησιμοποιείται με σκοπό την ανάκτηση της επιστρεφόμενης τιμής από το Callable αντικείμενο  και χρησιμοποιείται για την διαχείριση της κατάστασης των Callable και Runnable εργασιών.
Η διασύνδεση ExecutorService παρέχει έναν αριθμό μεθόδων για την διαχείριση του τερματισμού ενός αντικειμένου εκτελεστή. Αλλά για να υποστηριχθεί πλήρως η άμεση διακοπή του, θα πρέπει οι διάφορες εργασίες μιας εφαρμογής να διαχειρίζονται της διακοπές σωστά.

Η διασύνδεση ScheduledExecutorService

H διασύνδεση ScheduledExecutorService εμπλουτίζει τις μεθόδους από τη γονική της διασύνδεση ExecutorService με την χρήση χρονοπρογραμματισμού. Δηλαδή εκτελεί μια Runnable ή Callable εργασία με μια συγκεκριμένη καθυστερήση. Επιπλέον, η συγκεκριμένη διασύνδεση ορίζει τις μεθόδους scheduleAtFixedRate και την scheduleWithFixedDelay, οι οποίες εκτελούν συγκεκριμένες εργασίες κατά επανάληψη, σε τακτά χρονικά διαστήματα.

Δεξαμενές Νημάτων (Thread Pools)

Οι περισσότερες υλοποιήσεις εκτελεστών (executor) στο πακέτο java.util.concurrent χρησιμοποιούν δεξαμενές νημάτων. Οι δεξαμενές νημάτων απαρτίζονται από νήματα – εργάτες (worket threads). Τέτοιου είδους νήματα υφίστανται αναξέρτητα από τις Runnable και Callable εργασίες που εκτελούνται και έτσι μπορούν να εκτελούν πολλαπλές εργασίες.
Χρησιμοποιώντας νήματα – εργάτες ελαχιστοποιούμε την επιβάρυνση εξαιτίας της δημιουργίας νέων νημάτων. Τα αντικείμενα νημάτων χρησιμοποιούν ένα σημαντικό ποσό μνήμης και σε μεγάλες εφαρμογές η δημιουργία και η αποδέσμευση αντικειμένων νημάτων δημιουργεί μία σημαντική επιβάρυνση διαχείρισης της μνήμης. Ένας συχνός και κοινός τύπος δεξαμενής νημάτων είναι μία σταθερή / στατική δεξαμενή νημάτων (fixed thread pool). Αυτού του είδους η δεξαμενή έχει ένα καθορισμένο αριθμό από ενεργά νήματα. Οι εργασίες υποβάλλονται στην δεξαμενή διαμέσου μιας εσωτερικής ουράς προτεραιότητας, ενώ ακόμη μπορεί η δεξαμενή να συγκρατήσει κάποιες εργασίες όταν υπάρχουν περισσότερες εργασίες από ότι νήματα.
Ένα σημαντικό πλεονέκτημα από την σταθερή δεξαμενή νημάτων είναι ότι οι εφαρμογές που τη χρησιμοποιούν υποβαθμίζονται ομαλά (degrade gracefully). Για παράδειγμα έστω μια εφαρμογή web server όπου κάθε HTTP αίτημα αντιμετωπίζεται από ένα ξεχωριστό νήμα. Εάν η εφαρμογή δημιουργεί ένα νέο νήμα για κάθε HTTP αίτημα, και η εφαρμογή δέχεται περισσότερα αιτήματα από αυτά που μπορεί να χειριστεί, τότε αμέσως η εφαρμογή ξαφνικά θα σταματήσει να αποκρίνεται σε όλα τα αιτήματα όταν η επιβάρυνση των νημάτων υπερβεί τα όρια της εφαρμογής και του συστήματος. Με το όριο στον αριθμό των νημάτων που μπορούν να δημιουργηθούν, η εφαρμογή δεν θα μπορεί να εξυπηρετεί HTTP αιτήματα τόσο γρήγορα όσο αυτά καταφτάνουν. Αλλά θα μπορεί να τα εξυπηρετεί τόσο γρήγορα όσο το σύστημα μπορεί να τα υποστηρίξει.
Ένας απλός τρόπος για να δημιουργήσουμε ένα executor που χρησιμοποιεί μία σταθερή δεξαμενή νημάτων είναι να καλέσουμε την εργοστασιακή μέθοδο newFixedThreadPool από το πακέτο java.util.concurrent.Executors. Αυτή η κλάση επισης παρέχει τις εξής εργοστασιακές μεθόδους:
Εάν κανένας από τους executors που παρέχονται από τις παραπάνω εργοστασιακές μεθόδους δεν ανταποκρίνεται στις απαιτήσεις μας, τα στιγμιότυπα από τις java.util.concurrent.ThreadPoolExecutor είτε java.util.concurrent.ScheduledThreadPoolExecutor θα μας δώσουν επιπλέον επιλογές.

Ακολουθεί ένα παράδειγμα χρήσης executor για τη δημιουργία ενός thread pool. Η κλάση
WorkerThread δημιουργεί απλά νήματα που δέχονται ως παράμετρο ένα String εμφανίζουν ένα μήνυμα εκκίνησης, αναστέλουν την εκτέλεσή τους και μετά εμφανίζουν ένα μήνυμα τερματισμού.
public class WorkerThread implements Runnable {

    private String command;

    public WorkerThread(String s){
        this.command=s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+' Start. Command = '+command);
        processCommand();
        System.out.println(Thread.currentThread().getName()+' End.');
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString(){
        return this.command;
    }
}
Η κλάση SimpleThreadPool μέσω της μεθόδου ExecutorService δημιουργεί ένα  thread pool 5 νημάτων που εκτελούν 10 στιγμιότυπα (νήματα) της κλάσης WorkerThread.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleThreadPool {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkerThread('' + i);
            executor.execute(worker);
          }
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println('Finished all threads');
    }

}

Διακλάδωση/Ένωση (Fork/Join)

Το fork/join είναι μια υλοποίηση της διασύνδεσης ExecutorService που μας βοηθά στο να εκμεταλλευτούμε τους πολλαπλούς πυρήνες. Είναι σχεδιασμένη για μια εργασία που μπορεί να διασπαστεί σε μικρότερα τμήματα επεναληπτικά ή αναδρομικά. Ο στόχος είναι να χρησιμοποιήσουμε όλους τους διαθέσιμους πόρους για να ενισχύσουμε την απόδοση της εφαρμογής μας.
Όπως με κάθε υλοποίηση ExecutorService, η δομή fork/join κατανέμει εργασίες σε νήματα – εργάτες σε μία δεξαμενή νημάτων. Η δομή fork/join είναι έχει ένα καθορισμένο τρόπο λειτουργίας (work-stealing), δηλαδή ο αλγόριθμος της δομής fork/join χρησιμοποιεί εργάτες – νήματα και κατανέμει το φόρτο εργασιών στα πολλαπλά νήματα ώστε όλα να είναι απασχολημένα.
Η κεντρική κλάση της δομής fork/join είναι η κλάση ForkJoinPool, είναι μια επέκταση της AbstractExecutorService κλάσης. Η κλάση ForkJoinPool υλοποιεί τον πυρήνα του work-stealing αλγορίθμου και μπορεί να εκτελεί διεργασίες ForkJoinTask.

Βασική Χρήση

Το πρώτο βήμα για την χρήση της δομής fork/join είναι αρχικά να γράψουμε το μέρος του κώδικα που απαρτίζει το υπολογιστικό μέρος του προγράμματός μας. Ένα τέτοιο παράδειγμα είναι το εξής:
if (my portion of the work is small enough)
  do the work directly
else
  split my work into two pieces
  invoke the two pieces and wait for the results
Στη συνέχεια ενσωματώνουμε αυτό το κώδικα σε μία ForkJoinTask υποκλάση, επίσης μπορούμε να χρησιμοποιήσουμε πιο εξειδικευμένες δομές όπως την RecursiveTask (η οποία επιστρέφει ένα αποτέλεσμα) είτε την RecursiveAction.
Όταν η υποκλάση ForkJoinTask είναι έτοιμη, τότε δημιουργήστε ένα αντικείμενο που αντιπροσωπεύει όλη την εργασία του προγράμματος που πρέπει να γίνει και ενσωματώστε εκεί την μέθοδο invoke() για το στιγμιότυπο της ForkJoinPool.

      Διαύγεια μετά από Θόλωση (Blurring for Clarity)

Για να κατανοήσουμε καλύτερα πως λειτουργεί η δομή fork/join, έστω ότι θέλουμε να θολώσουμε μια εικόνα. Η εικόνα αναπαρίσταται από ένα πίνακα ακεραίων, όπου κάθε ακέραιος περιέχει τιμές χρωμάτων για κάθε pixel. Η θολωμένη τελική εικόνα επίσης απαρτίζεται από ένα πίνακα ακεραίων ιδίου μεγέθους με την αρχική εικόνα.
Η θόλωση της εικόνας γίνεται μέσω της επεξεργασίας του αρχικού πίνακα ακεραίων κατά ένα pixel την φορά. Το χρώμα του κάθε pixel μετασχηματίζεται ως τον μέσο όρο των τιμών των χρωμάτων των γειτονικών pixels. Το αποτέλεσμα αποθηκεύεταιστον τελικό πίνακα ακεραίων. Αλλά η εικόνα είναι αποτελείται από ένα μεγάλο πίνακα, και αυτή η διαδικασία μπορεί να διαρκέσει αρκετό χρόνο. Μπορούμε να εκμεταλλευτούμε συντρλεχουσαη εκτέλεση από πολυπύρηνα συστήματα με την υλοποίηση του αλγορίθμου της δομής fork/join. Παρακάτω παρουσιάζεται μια πιθανή υλοποιήση:
public class ForkBlur extends RecursiveAction {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;
  
    // Processing window size; should be odd.
    private int mBlurWidth = 15;
  
    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    protected void computeDirectly() {
        int sidePixels = (mBlurWidth - 1) / 2;
        for (int index = mStart; index < mStart + mLength; index++) {
            // Calculate average.
            float rt = 0, gt = 0, bt = 0;
            for (int mi = -sidePixels; mi <= sidePixels; mi++) {
                int mindex = Math.min(Math.max(mi + index, 0),
                                    mSource.length - 1);
                int pixel = mSource[mindex];
                rt += (float)((pixel & 0x00ff0000) >> 16)
                      / mBlurWidth;
                gt += (float)((pixel & 0x0000ff00) >>  8)
                      / mBlurWidth;
                bt += (float)((pixel & 0x000000ff) >>  0)
                      / mBlurWidth;
            }
          
            // Reassemble destination pixel.
            int dpixel = (0xff000000     ) |
                   (((int)rt) << 16) |
                   (((int)gt) <<  8) |
                   (((int)bt) <<  0);
            mDestination[index] = dpixel;
        }
    }
  
  ...
Εν συνεχεία υλοποιούμε την αφηρημένη μέθοδο compute(), η οποία είτε εφαρμόζει αμέσως την θόλωση είτε την συγκεκριμένη εργασία την χωρίζει σε δύο μικρότερες διαδικασίες. Ένας απλός τρόπος για να καθορίσουμε εάν πρέπει η συγκεκριμένη εργασία να χωριστεί σε δύο μέρη είτε πρέπει άμεσα να εκτελεστεί είναι η εξής:
protected static int sThreshold = 100000;

protected void compute() {
    if (mLength < sThreshold) {
        computeDirectly();
        return;
    }
    
    int split = mLength / 2;
    
    invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
              new ForkBlur(mSource, mStart + split, mLength - split,
                           mDestination));
}
Για να ρυθμίσουμε μια εργασία να εκτελεστεί μέσα σε μια δεξαμενή νημάτων fork/join (ForkJoinPool), τα βήματα που πρέπει να ακολουθήσουμε είναι τα εξής:
  1. Δημιουργία του κώδικα – εργασία που πρέπει να διεκπεραιωθεί π.χ.
    // source image pixels are in src
    // destination image pixels are in dst
    ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
  2. Δημιουργία της ForkJoinPool που θα εκτελέσει την συγκεκριμένη εργασία
    ForkJoinPool pool = new ForkJoinPool();
  3. Και εκτέλεση της εργασίας
    pool.invoke(fb);
Ο πηγαίος κώδικας περιλαμβάνει μερικές επιπλέον εντολές που δημιουργούν τη τελική εικόνα, ForkBlur.

Εφαρμογές της δομής fork/join

Εκτός από τη χρήση της δομής fork/join για συντρέχουσες εφαρμογές σε ένα πολυπύρηνο σύστημα όπως το προηγούμενο παράδειγμα, υπάρχουν αρκετά χαρακτηριστικά στην Java SE, όπου υλοποιούνται με βάση την δομή της fork/join. Μία τέτοια υλοποίηση που έχει εισαχθεί από την Java SE 8, χρησιμοποιείτε στην κλάση java.util.Arrays για τις μεθόδους του parallelSort(). Αυτές οι μέθοδοι είναι παρόμοιες με τη μέθοδο sort(), αλλά εισάγει παραλληλισμό μέσω της δομής fork/join. Η παράλληλη ταξινόμηση για μεγάλους πίνακες λειτουργεί πιο γρήγορα από την ακολουθιακή ταξινόμηση όταν εκτελείται σε πολυπύρηνα συστήματα. Ωστόσο πως ακριβώς η δομή fork/join χρησιμοποιείται από αυτές τις μεθόδους είναι πέρα από το πεδίο του συγκεκριμένου βοηθήματος. Για περισσότερες πληροφορίες δείτε την τεκμηρίωση της Java API.
Μια άλλη υλοποίηση της δομής fork/join χρησιμοποιείτε από τις μεθόδους του πακέτου java.util.streams, το οποίο είναι μέρος από το Project Lambda για την έκδοση Java SE 8. Για περισσότερες πληροφορίες, δείτε το τμήμα Lambda Expressions.

Συντρέχουσες Συλλογές  (Concurrent Collections)

Το πακέτο java.util.concurrent περιλαμβάνει ένα σύνολο από επιπρόσθετα εργαλεία στην βασική δομή της Java Collections Framework. Τα συγκεκριμένα εργαλεία κατηγοροποιούνται σε συλλογές διεπαφών που είναι οι εξής:
Όλες αυτές οι συλλογές μας βοηθούν για την αποφυγή συνθηκών ανταγωνισμού μεταξύ των νημάτων με τον ορισμό μιας σχέσης τύπου συμβαίνει-πριν μεταξύ μιας λειτουργίας που προσθέτει ένα αντικείμενο σε μία συλλογή δεδομένων με επακόλουθες λειτουργίες που έχουν πρόσβαση σε αυτό το αντικείμενο.

Ακολουθεί μια απλή υλοποίηση του προβλήματος Παραγωγού Καταναλωτή με χρηση
BlockingQueue.
public class BlockingQueueExample {

    public static void main(String[] args) throws Exception {

        BlockingQueue queue = new ArrayBlockingQueue(1024);

        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);

        new Thread(producer).start();
        new Thread(consumer).start();

        Thread.sleep(4000);
    }
}
public class Producer implements Runnable{

    protected BlockingQueue queue = null;

    public Producer(BlockingQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            queue.put("1");
            Thread.sleep(1000);
            queue.put("2");
            Thread.sleep(1000);
            queue.put("3");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Consumer implements Runnable{

    protected BlockingQueue queue = null;

    public Consumer(BlockingQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            System.out.println(queue.take());
            System.out.println(queue.take());
            System.out.println(queue.take());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Ατομικές Μεταβλητές

Το πακέτο java.util.concurrent.atomic, ορίζει κλάσεις που υποστηρίζουν ατομικές δραστηριότητες σε μία μεταβλητή. Σε αυτό το πακέτο όλες οι κλάσεις έχουν get και set μεθόδους που λειτουργούν ως αναγνώσεις και εγγραφές σε μεταβλητές. Αλλά τώρα αυτές οι μέθοδοι δημιουργούν μια σχέση τύπου συμβαίνει-πριν μεταξύ των μεταβλητών, όπως επίσης και με κάθε επακόλουθη μέθοδο get & set. Παραδείγματος χάριν, η ατομική μέθοδος compareAndSet επίσης έχει χαρακτηριστικά συνέπειας μνήμης ώστε εκτελεί ατομικές μεθόδους αριθμητικής που εφαρμόζονται σε ατομικές ακέραιες μεταβλητές.
Παρατηρούμε την προηγούμενη κλάση Counter, που είναι ένα παράδειγμα για τις συνθήκες παρεμβολής και ανταγωνισμού μεταξύ των νημάτων:
class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}
Ένας τρόπος για να μετατρέψουμε την κλάση Counter ασφαλής από παρεμβολές μεταξύ των νημάτων και από συνθήκες ανταγωνισμού είναι να μετατρέψουμε τις μεθόδους της σε synchronized, όπως παρακάτω παρουσιάζεται με την κλάση SynchronizedCounter
class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }

}
Για μια τόσο απλή κλάση, ο συγχρονισμός είναι μια αποδεκτή λύση, αλλά για πιο πολύπλοκες κλάσεις, θα ήταν καλό να αποφύγουμε ένα ανώφελο αντίκτυπο στην ζωτικότητα των νημάτων με περιττό συγχρονισμό. Αντικαθιστώντας το int πεδίο με AtomicInteger μας επιτρέπει να αποφύγουμε παρεμβολές μεταξύ των νημάτων χωρίς να καταφύγουμε στον συγχρονισμό τους. Όπως παρακάτω παρουσιάζεται με την κλάση AtomicCounter:
class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

Συντρέχοντες Τυχαίοι Αριθμοί

Στην JDK 7, το πακέτο java.util.concurrent περιλαμβάνει μια εύχρηστη και βολική κλάση, την ThreadLocalRandom, ειδική για εφαρμογές που χρησιμοποιούν τυχαίους αριθμούς από πολλάπλά νήματα είτε για προγράμματα με εργασίες fork/join (ForkJoinTasks).
Για συντρέχουσα πρόσβασή σε τυχαίες μεταβλητές, χρησιμοποιούμε την ThreadLocalRandom αντί για την Math.random(). Ως αποτέλεσμα έχουμε λιγότερη συμφόρηση από τα νήματα προς μία κλάση και τελικώς καλύτερη απόδοση.
Το μόνο που χρειάζεται για να καλέσουμε ένα τέτοιο τυχαίο αριθμό είναι να καλέσουμε την ThreadLocalRandom.current(), εν συνεχεία καλούμε μια από τις μεθόδους της. Για παράδειγμα:
int r = ThreadLocalRandom.current().nextInt(4, 77);

Περισσότερες Πηγές