9 λάθη που πρέπει να αποφύγετε κατά τον προγραμματισμό σε Java
Ποια λάθη πρέπει να αποφεύγονται κατά τον προγραμματισμό σε Java; Στο παρακάτω κομμάτι απαντάμε σε αυτό το ερώτημα.

Διαβάστε το πρώτο μέρος της σειράς ιστολογίων μας που είναι αφιερωμένο στην ταυτόχρονη χρήση της Java. Στο επόμενο άρθρο θα δούμε αναλυτικότερα τις διαφορές μεταξύ νημάτων και διεργασιών, τις δεξαμενές νημάτων, τους εκτελεστές και πολλά άλλα!
Γενικά, η συμβατική προσέγγιση προγραμματισμού είναι διαδοχική. Τα πάντα σε ένα πρόγραμμα συμβαίνουν ένα βήμα τη φορά.
Αλλά, στην πραγματικότητα, η παράλληλη λειτουργία είναι ο τρόπος με τον οποίο λειτουργεί όλος ο κόσμος - είναι η ικανότητα να εκτελούνται ταυτόχρονα περισσότερες από μία εργασίες.
Για να συζητήσετε προχωρημένα θέματα όπως συγχρονισμός σε Java ή πολυνηματικότητα, πρέπει να καταλήξουμε σε ορισμένους κοινούς ορισμούς για να είμαστε σίγουροι ότι είμαστε στην ίδια σελίδα.
Ας ξεκινήσουμε με τα βασικά. Στον μη ακολουθιακό κόσμο, έχουμε δύο είδη αναπαραστατών ταυτόχρονης λειτουργίας: διεργασίες και
νήματα. Μια διεργασία είναι μια περίπτωση του προγράμματος που εκτελείται. Κανονικά, είναι απομονωμένη από άλλες διεργασίες.
Το λειτουργικό σύστημα είναι υπεύθυνο για την ανάθεση πόρων σε κάθε διεργασία. Επιπλέον, ενεργεί ως αγωγός που
τους προγραμματίζει και τους ελέγχει.
Το νήμα είναι ένα είδος διεργασίας, αλλά σε χαμηλότερο επίπεδο, γι' αυτό και είναι επίσης γνωστό ως ελαφρύ νήμα. Πολλαπλά νήματα μπορούν να εκτελούνται σε ένα
διαδικασία. Εδώ το πρόγραμμα ενεργεί ως χρονοπρογραμματιστής και ελεγκτής για τα νήματα. Με αυτόν τον τρόπο τα μεμονωμένα προγράμματα φαίνεται να κάνουν
πολλαπλές εργασίες ταυτόχρονα.
Η θεμελιώδης διαφορά μεταξύ νημάτων και διεργασιών είναι το επίπεδο απομόνωσης. Η διεργασία έχει το δικό της σύνολο
πόρους, ενώ το νήμα μοιράζεται δεδομένα με άλλα νήματα. Μπορεί να φαίνεται σαν μια προσέγγιση επιρρεπής σε σφάλματα και όντως είναι. Για το
τώρα, ας μην επικεντρωθούμε σε αυτό, καθώς είναι πέρα από το πεδίο εφαρμογής αυτού του άρθρου.
Διαδικασίες, νήματα - εντάξει... Αλλά τι ακριβώς είναι η ταυτόχρονη λειτουργία; Ταυτόχρονη εκτέλεση σημαίνει ότι μπορείτε να εκτελείτε πολλαπλές εργασίες ταυτόχρονα.
ώρα. Αυτό δεν σημαίνει ότι οι εργασίες αυτές πρέπει να εκτελούνται ταυτόχρονα - αυτό είναι ο παραλληλισμός. Concurrenc στη Javay επίσης δεν
απαιτούν να έχετε πολλαπλές CPU ή ακόμη και πολλαπλούς πυρήνες. Μπορεί να επιτευχθεί σε περιβάλλον ενός πυρήνα με την αξιοποίηση
εναλλαγή περιβάλλοντος.
Ένας όρος που σχετίζεται με την ταυτόχρονη εκτέλεση είναι το multithreading. Πρόκειται για ένα χαρακτηριστικό των προγραμμάτων που τους επιτρέπει να εκτελούν πολλές εργασίες ταυτόχρονα. Δεν χρησιμοποιούν όλα τα προγράμματα αυτή την προσέγγιση, αλλά αυτά που το κάνουν μπορούν να αποκαλούνται πολυνηματικά.
Είμαστε σχεδόν έτοιμοι να φύγουμε, μόνο ένας ορισμός ακόμα. Ασυνγχρονισμός σημαίνει ότι ένα πρόγραμμα εκτελεί λειτουργίες που δεν μπλοκάρουν.
Εκκινεί μια εργασία και στη συνέχεια συνεχίζει με άλλα πράγματα περιμένοντας την απάντηση. Όταν λάβει την απάντηση, μπορεί να αντιδράσει σε αυτήν.
Από προεπιλογή, κάθε Εφαρμογή Java τρέχει σε μία διαδικασία. Σε αυτή τη διεργασία, υπάρχει ένα νήμα που σχετίζεται με το main()
μέθοδος της
μια εφαρμογή. Ωστόσο, όπως αναφέρθηκε, είναι δυνατή η αξιοποίηση των μηχανισμών πολλαπλών νημάτων μέσα σε ένα
πρόγραμμα.
Νήμα
είναι μια Java κλάση στην οποία συμβαίνει η μαγεία. Αυτή είναι η αντικειμενική αναπαράσταση του προαναφερθέντος νήματος. Στο
να δημιουργήσετε το δικό σας νήμα, μπορείτε να επεκτείνετε την Νήμα
κατηγορία. Ωστόσο, δεν συνιστάται αυτή η προσέγγιση. Νήματα
θα πρέπει να χρησιμοποιηθεί ως μηχανισμός εκτέλεσης της εργασίας. Οι εργασίες είναι κομμάτια κωδικός που θέλουμε να τρέξουμε σε ταυτόχρονη λειτουργία. Μπορούμε να τα ορίσουμε χρησιμοποιώντας την εντολή Εκτελέσιμο
διεπαφή.
Αρκετά όμως με τη θεωρία, ας βάλουμε τον κώδικά μας εκεί που είναι το στόμα μας.
Ας υποθέσουμε ότι έχουμε δύο πίνακες αριθμών. Για κάθε πίνακα, θέλουμε να γνωρίζουμε το άθροισμα των αριθμών σε έναν πίνακα. Ας
υποτίθεται ότι υπάρχουν πολλές τέτοιες συστοιχίες και κάθε μία από αυτές είναι σχετικά μεγάλη. Σε τέτοιες συνθήκες, θέλουμε να κάνουμε χρήση της ταυτόχρονης εκτέλεσης και να αθροίσουμε κάθε πίνακα ως ξεχωριστή εργασία.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7},
Runnable task1 = () -> {
int sum = Arrays.stream(a1).sum(),
System.out.println("1. Το άθροισμα είναι: " + sum),
};
Runnable task2 = () -> {
int sum = Arrays.stream(a2).sum(),
System.out.println("2. Το άθροισμα είναι: " + sum),
};
Runnable task3 = () -> {
int sum = Arrays.stream(a3).sum(),
System.out.println("3. Το άθροισμα είναι: " + sum),
};
new Thread(task1).start(),
new Thread(task2).start(),
new Thread(task3).start(),
Όπως μπορείτε να δείτε από τον παραπάνω κώδικα Εκτελέσιμο
είναι μια λειτουργική διεπαφή. Περιέχει μία μόνο αφηρημένη μέθοδο run()
χωρίς επιχειρήματα. Το Εκτελέσιμο
διεπαφή θα πρέπει να υλοποιείται από κάθε κλάση της οποίας οι περιπτώσεις προορίζονται να είναι
εκτελείται από ένα νήμα.
Αφού ορίσετε μια εργασία, μπορείτε να δημιουργήσετε ένα νήμα για την εκτέλεσή της. Αυτό μπορεί να επιτευχθεί μέσω νέο νήμα()
κατασκευαστή που
παίρνει Εκτελέσιμο
ως επιχείρημά του.
Το τελικό βήμα είναι να start()
ένα πρόσφατα δημιουργημένο νήμα. Στο API υπάρχουν επίσης run()
μέθοδοι στο Εκτελέσιμο
και σεΝήμα
. Ωστόσο, αυτός δεν είναι ένας τρόπος για να αξιοποιήσετε την ταυτόχρονη χρήση της Java. Μια άμεση κλήση σε κάθε μία από αυτές τις μεθόδους οδηγεί σε
εκτελώντας την εργασία στο ίδιο νήμα το main()
η μέθοδος εκτελείται.
Όταν υπάρχουν πολλές εργασίες, η δημιουργία ξεχωριστού νήματος για κάθε μία δεν είναι καλή ιδέα. Η δημιουργία ενός Νήμα
είναι μια
βαριά λειτουργία και είναι πολύ καλύτερο να επαναχρησιμοποιείτε τα υπάρχοντα νήματα παρά να δημιουργείτε νέα.
Όταν ένα πρόγραμμα δημιουργεί πολλά νήματα μικρής διάρκειας, είναι προτιμότερο να χρησιμοποιείτε μια δεξαμενή νημάτων. Η δεξαμενή νημάτων περιέχει έναν αριθμό
έτοιμα προς εκτέλεση αλλά μη ενεργά νήματα. Δίνοντας ένα Εκτελέσιμο
στη δεξαμενή προκαλεί ένα από τα νήματα να καλέσει την εντολήrun()
μέθοδος της δεδομένης Εκτελέσιμο
. Μετά την ολοκλήρωση μιας εργασίας το νήμα εξακολουθεί να υπάρχει και να βρίσκεται σε κατάσταση αδράνειας.
Εντάξει, το καταλάβατε - προτιμήστε τη συγκέντρωση νημάτων αντί για χειροκίνητη δημιουργία. Αλλά πώς μπορείτε να χρησιμοποιήσετε τις δεξαμενές νημάτων; Το Εκτελεστές
έχει έναν αριθμό στατικών μεθόδων εργοστασίου για την κατασκευή δεξαμενών νημάτων. Για παράδειγμα newCachedThredPool()
δημιουργεί το
μια δεξαμενή στην οποία δημιουργούνται νέα νήματα ανάλογα με τις ανάγκες και τα αδρανή νήματα διατηρούνται για 60 δευτερόλεπτα. Αντίθετα,newFixedThreadPool()
περιέχει ένα σταθερό σύνολο νημάτων, στο οποίο τα αδρανή νήματα διατηρούνται επ' αόριστον.
Ας δούμε πώς θα μπορούσε να λειτουργήσει στο παράδειγμά μας. Τώρα δεν χρειάζεται να δημιουργούμε νήματα με το χέρι. Αντ' αυτού, πρέπει να δημιουργήσουμεExecutorService
η οποία παρέχει μια δεξαμενή νημάτων. Στη συνέχεια, μπορούμε να αναθέσουμε εργασίες σε αυτό. Το τελευταίο βήμα είναι να κλείσουμε το νήμα
pool για να αποφεύγονται οι διαρροές μνήμης. Ο υπόλοιπος προηγούμενος κώδικας παραμένει ο ίδιος.
ExecutorService executor = Executors.newCachedThreadPool(),
executor.submit(task1),
executor.submit(task2),
executor.submit(task3),
executor.shutdown(),
Εκτελέσιμο
μοιάζει με έναν έξυπνο τρόπο δημιουργίας ταυτόχρονων εργασιών, αλλά έχει ένα σημαντικό μειονέκτημα. Δεν μπορεί να επιστρέψει καμία
αξία. Επιπλέον, δεν μπορούμε να προσδιορίσουμε αν μια εργασία έχει τελειώσει ή όχι. Επίσης, δεν γνωρίζουμε αν έχει ολοκληρωθεί
κανονικά ή κατ' εξαίρεση. Η λύση σε αυτά τα δεινά είναι Ανακλητό
.
Ανακλητό
είναι παρόμοια με Εκτελέσιμο
κατά κάποιο τρόπο περιτυλίγει επίσης ασύγχρονες εργασίες. Η κύρια διαφορά είναι ότι είναι σε θέση να
επιστρέφουν μια τιμή. Η τιμή επιστροφής μπορεί να είναι οποιουδήποτε (μη πρωταρχικού) τύπου όπως η Ανακλητό
είναι ένας παραμετροποιημένος τύπος.Ανακλητό
είναι μια λειτουργική διεπαφή που έχει call()
μέθοδος η οποία μπορεί να πετάξει ένα Εξαίρεση
.
Τώρα ας δούμε πώς μπορούμε να αξιοποιήσουμε Ανακλητό
στο πρόβλημα της συστοιχίας μας.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7},
Callable task1 = () -> Arrays.stream(a1).sum(),
Callable task2 = () -> Arrays.stream(a2).sum(),
Callable task3 = () -> Arrays.stream(a3).sum(),
ExecutorService executor = Executors.newCachedThreadPool(),
Future future1 = executor.submit(task1),
Future future2 = executor.submit(task2),
Future future3 = executor.submit(task3),
System.out.println("1. Το άθροισμα είναι: " + future1.get()),
System.out.println("2. Το άθροισμα είναι: " + future2.get()),
System.out.println("3. Το άθροισμα είναι: " + future3.get()),
executor.shutdown(),
Εντάξει, μπορούμε να δούμε πώς Ανακλητό
δημιουργείται και στη συνέχεια υποβάλλεται στο ExecutorService
. Αλλά τι στο καλό είναι Μελλοντικό
?Μελλοντικό
λειτουργεί ως γέφυρα μεταξύ των νημάτων. Το άθροισμα κάθε πίνακα παράγεται σε ξεχωριστό νήμα και χρειαζόμαστε έναν τρόπο για να
να στείλετε τα αποτελέσματα αυτά πίσω στο main()
.
Για να ανακτήσετε το αποτέλεσμα από Μελλοντικό
πρέπει να καλέσουμε get()
μέθοδος. Εδώ μπορούν να συμβούν δύο πράγματα. Πρώτον, η
το αποτέλεσμα του υπολογισμού που εκτελείται από Ανακλητό
είναι διαθέσιμη. Τότε το παίρνουμε αμέσως. Δεύτερον, το αποτέλεσμα δεν είναι
έτοιμο ακόμα. Σε αυτή την περίπτωση get()
θα μπλοκάρει μέχρι να είναι διαθέσιμο το αποτέλεσμα.
Το θέμα με Μελλοντικό
είναι ότι λειτουργεί με το παράδειγμα της "ώθησης". Όταν χρησιμοποιείται Μελλοντικό
πρέπει να είσαι σαν ένα αφεντικό που
ρωτάει συνεχώς: "Τελείωσε η εργασία σας; Είναι έτοιμο;' μέχρι να δώσει ένα αποτέλεσμα. Η δράση υπό συνεχή πίεση είναι
ακριβά. Πολύ καλύτερη προσέγγιση θα ήταν να παραγγείλετε Μελλοντικό
τι να κάνει όταν είναι έτοιμο με το έργο του. Δυστυχώς,Μελλοντικό
δεν μπορεί να το κάνει αυτό αλλά ComputableFuture
μπορεί.
ComputableFuture
λειτουργεί με το "παράδειγμα έλξης". Μπορούμε να του πούμε τι να κάνει με το αποτέλεσμα όταν ολοκληρώσει τις εργασίες του. Είναι
είναι ένα παράδειγμα ασύγχρονης προσέγγισης.
ComputableFuture
λειτουργεί άψογα με Εκτελέσιμο
αλλά όχι με Ανακλητό
. Αντ' αυτού, είναι δυνατόν να παρέχετε μια εργασία στοComputableFuture
με τη μορφή Προμηθευτής
.
Ας δούμε πώς σχετίζονται τα παραπάνω με το πρόβλημά μας.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7},
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println),
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println),
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println),
Το πρώτο πράγμα που σας κάνει εντύπωση είναι πόσο μικρότερη είναι αυτή η λύση. Εκτός αυτού, φαίνεται επίσης τακτοποιημένη και τακτοποιημένη.
Εργασία προς ΟλοκληρώσιμοΜέλλον
μπορεί να παρασχεθεί από supplyAsync()
μέθοδος που παίρνει Προμηθευτής
ή με runAsync()
που
παίρνει Εκτελέσιμο
. Ένα callback - ένα κομμάτι κώδικα που θα πρέπει να εκτελεστεί με την ολοκλήρωση της εργασίας - ορίζεται από thenAccept()
μέθοδος.
Java παρέχει πολλές διαφορετικές προσεγγίσεις για την ταυτόχρονη χρήση. Σε αυτό το άρθρο, μόλις που αγγίξαμε το θέμα.
Παρ' όλα αυτά, καλύψαμε τα βασικά Νήμα
, Εκτελέσιμο
, Ανακλητό
, και CallableFuture
το οποίο θέτει ένα καλό σημείο
για περαιτέρω διερεύνηση του θέματος.