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

Java 101: Ταυτόχρονη Java χωρίς πόνο, Μέρος 1

Με την αυξανόμενη πολυπλοκότητα των ταυτόχρονων εφαρμογών, πολλοί προγραμματιστές θεωρούν ότι οι δυνατότητες νήματος χαμηλού επιπέδου Java δεν επαρκούν για τις ανάγκες προγραμματισμού τους. Σε αυτήν την περίπτωση, ίσως είναι καιρός να ανακαλύψετε τα Java Concurrency Utilities. Ξεκινήστε με java.util.concurrent, με τη λεπτομερή εισαγωγή του Jeff Friesen στο πλαίσιο Executor, τους τύπους συγχρονισμού και το πακέτο Java Concurrent Collection.

Java 101: Η επόμενη γενιά

Το πρώτο άρθρο σε αυτήν τη νέα σειρά JavaWorld παρουσιάζει το API ημερομηνίας και ώρας Java.

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

  • Πρωτόγονα ταυτόχρονα χαμηλού επιπέδου Java (συγχρονισμένος, πτητικός, Περίμενε(), κοινοποιώ(), και ειδοποίησηΌλα ()) δεν είναι εύκολο στη χρήση σωστά. Οι κίνδυνοι σπειρώματος όπως το αδιέξοδο, η πείνα νήματος και οι συνθήκες αγώνα, που προκύπτουν από εσφαλμένη χρήση πρωτόγονων, είναι επίσης δύσκολο να εντοπιστούν και να εντοπιστούν σφάλματα.
  • Βασίζομαι συγχρονισμένος Ο συντονισμός της πρόσβασης μεταξύ νημάτων οδηγεί σε ζητήματα απόδοσης που επηρεάζουν την επεκτασιμότητα εφαρμογών, μια απαίτηση για πολλές σύγχρονες εφαρμογές.
  • Οι βασικές δυνατότητες σπείρωμα της Java είναι πολύ χαμηλό επίπεδο. Οι προγραμματιστές συχνά χρειάζονται κατασκευές υψηλότερου επιπέδου όπως σηματοφόροι και ομαδοποιήσεις νήματος, τις οποίες δεν προσφέρουν οι δυνατότητες σπειρώματος χαμηλού επιπέδου της Java. Ως αποτέλεσμα, οι προγραμματιστές θα δημιουργήσουν τις δικές τους κατασκευές, οι οποίες είναι χρονοβόρες και επιρρεπείς σε σφάλματα.

Το πλαίσιο JSR 166: Concurrency Utilities σχεδιάστηκε για να καλύψει την ανάγκη για υψηλού επιπέδου εγκατάσταση σπειρώματος. Ξεκίνησε στις αρχές του 2002, το πλαίσιο τυποποιήθηκε και εφαρμόστηκε δύο χρόνια αργότερα στην Java 5. Ακολούθησαν βελτιώσεις στην Java 6, στην Java 7 και στην επικείμενη Java 8.

Αυτό το δύο μέρη Java 101: Η επόμενη γενιά Η σειρά εισάγει προγραμματιστές λογισμικού εξοικειωμένους με το βασικό νήμα Java στα πακέτα και το πλαίσιο Java Concurrency Utilities. Στο Μέρος 1, παρουσιάζω μια επισκόπηση του πλαισίου Java Concurrency Utilities και παρουσιάζω το πλαίσιο Executor, τα βοηθητικά προγράμματα συγχρονισμού και το πακέτο Java Concurrent Collection.

Κατανόηση των νημάτων Java

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

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

Μέσα στα βοηθητικά προγράμματα Java Concurrency

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

Οι τύποι στο Java Concurrency Utilities οργανώνονται σε μικρά πλαίσια. δηλαδή, πλαίσιο Executor, συγχρονιστής, ταυτόχρονες συλλογές, κλειδαριές, ατομικές μεταβλητές και Fork / Join. Οργανώνονται περαιτέρω σε ένα κύριο πακέτο και σε ένα ζευγάρι από τα πακέτα:

  • java.util.concurrent περιέχει τύπους βοηθητικών προγραμμάτων υψηλού επιπέδου που χρησιμοποιούνται συνήθως στον ταυτόχρονο προγραμματισμό. Στα παραδείγματα περιλαμβάνονται οι σηματοφόροι, τα φράγματα, οι δεξαμενές νημάτων και οι ταυτόχρονοι κατακερματισμοί.
    • ο java.util.concurrent.atomic Το subpackage περιέχει κατηγορίες βοηθητικών προγραμμάτων χαμηλού επιπέδου που υποστηρίζουν προγραμματισμό χωρίς νήμα χωρίς κλειδώματα σε μεμονωμένες μεταβλητές.
    • ο java.util.concurrent.locks Το subpackage περιέχει τύπους βοηθητικών προγραμμάτων χαμηλού επιπέδου για κλείδωμα και αναμονή για συνθήκες, οι οποίοι διαφέρουν από τη χρήση συγχρονισμού και οθονών χαμηλού επιπέδου Java.

Το πλαίσιο Java Concurrency Utilities εκθέτει επίσης το χαμηλό επίπεδο σύγκριση και ανταλλαγή (CAS) οδηγίες υλικού, παραλλαγές των οποίων συνήθως υποστηρίζονται από σύγχρονους επεξεργαστές. Το CAS είναι πολύ πιο ελαφρύ από το μηχανισμό συγχρονισμού που βασίζεται στην οθόνη της Java και χρησιμοποιείται για την εφαρμογή ορισμένων πολύ κλιμακούμενων ταυτόχρονων κλάσεων. Με βάση το CAS java.util.concurrent.locks.ReentrantLock Η τάξη, για παράδειγμα, είναι πιο αποτελεσματική από την αντίστοιχη οθόνη συγχρονισμένος πρωτόγονος. ReentrantLock προσφέρει μεγαλύτερο έλεγχο στο κλείδωμα. (Στο Μέρος 2 θα εξηγήσω περισσότερα για το πώς λειτουργεί το CAS java.util.concurrent.)

System.nanoTime ()

Το πλαίσιο Java Concurrency Utilities περιλαμβάνει μεγάλο nanoTime (), το οποίο είναι μέλος του java.lang.System τάξη. Αυτή η μέθοδος επιτρέπει την πρόσβαση σε μια πηγή χρόνου νανοδευτερόλεπτου για την πραγματοποίηση σχετικών μετρήσεων χρόνου.

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

Το πλαίσιο του Executor

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

Λίστα 1. Server.java (Έκδοση 1)

εισαγωγή java.io.IOException; εισαγωγή java.net.ServerSocket; εισαγωγή java.net.Socket; class Server {public static void main (String [] args) ρίχνει το IOException {ServerSocket socket = νέο ServerSocket (9000); ενώ (true) {final Socket s = socket.accept (); Runnable r = new Runnable () {@ Override public void run () {doWork (s); }} νέο νήμα (r) .start (); }} στατικό άκυρο doWork (Socket s) {}}

Ο παραπάνω κώδικας περιγράφει μια απλή εφαρμογή διακομιστή (με doWork (Υποδοχή) άφησε άδειο για συντομία). Το νήμα διακομιστή καλεί επανειλημμένα socket.accept () για να περιμένετε ένα εισερχόμενο αίτημα και στη συνέχεια ξεκινά ένα νήμα για να εξυπηρετήσει αυτό το αίτημα όταν φτάσει.

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

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

java.util.concurrent περιλαμβάνει το πλαίσιο Executor, ένα μικρό πλαίσιο τύπων που αποσυνδέουν την υποβολή εργασιών από πολιτικές εκτέλεσης εργασιών. Χρησιμοποιώντας το πλαίσιο Executor, μπορείτε εύκολα να συντονίσετε την πολιτική εκτέλεσης εργασιών ενός προγράμματος χωρίς να χρειάζεται να ξαναγράψετε σημαντικά τον κώδικά σας.

Μέσα στο πλαίσιο του Executor

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

void execute (εντολή με δυνατότητα εκτέλεσης)

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

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

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

Πέντε από ΕξυπηρέτησηΟι μέθοδοι είναι ιδιαίτερα αξιοσημείωτες:

  • boolean awaitTermination (μεγάλο χρονικό όριο, μονάδα TimeUnit) μπλοκάρει το νήμα κλήσης έως ότου ολοκληρωθούν όλες οι εργασίες μετά από ένα αίτημα τερματισμού, το χρονικό όριο ή το τρέχον νήμα διακόπτεται, όποιο συμβεί πρώτο. Ο μέγιστος χρόνος αναμονής καθορίζεται από τέλος χρόνου, και αυτή η τιμή εκφράζεται στο μονάδα μονάδες που καθορίζονται από το TimeUnit enum; για παράδειγμα, TimeUnit.SECONDS. Αυτή η μέθοδος ρίχνει java.lang.InterruptException όταν διακόπτεται το τρέχον νήμα. Επιστρέφει αληθής όταν ο εκτελεστής τερματίζεται και ψευδής όταν λήξει το χρονικό όριο πριν από τον τερματισμό.
  • boolean isShutdown () επιστρέφει αληθής όταν ο εκτελεστής έχει κλείσει.
  • άκυρο κλείσιμο () ξεκινά μια ομαλή απενεργοποίηση στην οποία εκτελούνται εργασίες που έχουν υποβληθεί προηγουμένως, αλλά δεν γίνονται δεκτές νέες εργασίες.
  • Μελλοντική υποβολή (Callable task) υποβάλλει μια εργασία επιστροφής αξίας για εκτέλεση και επιστρέφει a Μελλοντικός αντιπροσωπεύοντας τα εκκρεμή αποτελέσματα της εργασίας.
  • Μελλοντική υποβολή (εκτελέσιμη εργασία) υποβάλλει α Τρέξιμο εργασία για εκτέλεση και επιστρέφει a Μελλοντικός που αντιπροσωπεύει αυτό το έργο.

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

ο Καλείται διεπαφή είναι παρόμοια με το Τρέξιμο διεπαφή στο ότι παρέχει μια μεμονωμένη μέθοδο που περιγράφει μια εργασία που πρέπει να εκτελεστεί. Διαφορετικός Τρέξιμο'μικρό άκυρη εκτέλεση () μέθοδος, Καλείται'μικρό Η κλήση V () ρίχνει την Εξαίρεση μέθοδος μπορεί να επιστρέψει μια τιμή και να ρίξει μια εξαίρεση.

Εργοστασιακές μέθοδοι

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

  • ExecutorService newCachedThreadPool () δημιουργεί ένα νήμα που δημιουργεί νέα νήματα όπως απαιτείται, αλλά το οποίο επαναχρησιμοποιεί νήματα που έχουν κατασκευαστεί προηγουμένως όταν είναι διαθέσιμα. Τα νήματα που δεν έχουν χρησιμοποιηθεί για 60 δευτερόλεπτα τερματίζονται και αφαιρούνται από την προσωρινή μνήμη. Αυτή η ομάδα νήματος βελτιώνει συνήθως την απόδοση των προγραμμάτων που εκτελούν πολλές βραχύβιες ασύγχρονες εργασίες.
  • ExecutorService newSingleThreadExecutor () δημιουργεί έναν εκτελεστή που χρησιμοποιεί ένα νήμα εργαζομένου που λειτουργεί από μια ουρά χωρίς περιορισμούς - οι εργασίες προστίθενται στην ουρά και εκτελούνται διαδοχικά (δεν είναι περισσότερες από μία εργασίες ενεργές ταυτόχρονα). Εάν αυτό το νήμα τερματιστεί λόγω αστοχίας κατά την εκτέλεση πριν από τον τερματισμό λειτουργίας του εκτελεστή, θα δημιουργηθεί ένα νέο νήμα για τη θέση του όταν πρέπει να εκτελεστούν οι επόμενες εργασίες.
  • ExecutorService newFixedThreadPool (int nThreads) δημιουργεί μια ομάδα νήματος που επαναχρησιμοποιεί έναν καθορισμένο αριθμό νημάτων που λειτουργούν από μια κοινόχρηστη ουρά χωρίς περιορισμούς. Στο μέγιστο ν νήματα Τα νήματα επεξεργάζονται ενεργά εργασίες. Εάν υποβληθούν επιπλέον εργασίες όταν όλα τα νήματα είναι ενεργά, περιμένουν στην ουρά μέχρι να είναι διαθέσιμο ένα νήμα. Εάν κάποιο νήμα τερματιστεί λόγω αποτυχίας κατά την εκτέλεση πριν από τον τερματισμό, θα δημιουργηθεί ένα νέο νήμα για να αντικατασταθεί όταν πρέπει να εκτελεστούν επόμενες εργασίες. Τα νήματα της πισίνας υπάρχουν μέχρι να κλείσει ο εκτελεστής.

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

Δείτε το java.util.concurrent Javadoc για να εξερευνήσετε επιπλέον τύπους.

Εργασία με το πλαίσιο Executor

Θα διαπιστώσετε ότι το πλαίσιο Executor είναι αρκετά εύκολο να δουλέψετε. Στη λίστα 2, έχω χρησιμοποιήσει Εκτελεστής διαθήκης και Εκτελεστές για να αντικαταστήσετε το παράδειγμα του διακομιστή από την Καταχώριση 1 με μια πιο επεκτάσιμη εναλλακτική βάση βάσει νήματος.

Λίστα 2. Server.java (Έκδοση 2)

εισαγωγή java.io.IOException; εισαγωγή java.net.ServerSocket; εισαγωγή java.net.Socket; εισαγωγή java.util.concurrent.Executor; εισαγωγή java.util.concurrent.Executors; διακομιστής κλάσης {static Executor pool = Executors.newFixedThreadPool (5); δημόσιος στατικός κενός κεντρικός (String [] args) ρίχνει το IOException {ServerSocket socket = new ServerSocket (9000); ενώ (true) {final Socket s = socket.accept (); Runnable r = new Runnable () {@ Override public void run () {doWork (s); }} pool.execute (r); }} στατικό άκυρο doWork (Socket s) {}}

Η λίστα 2 χρησιμοποιεί newFixedThreadPool (int) για να αποκτήσετε έναν εκτελεστή βασισμένο σε νήμα που επαναχρησιμοποιεί πέντε νήματα. Αντικαθιστά επίσης νέο νήμα (r) .start (); με pool.execute (r); για την εκτέλεση εργασιών με δυνατότητα εκτέλεσης μέσω οποιουδήποτε από αυτά τα νήματα.

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