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

Java Tip 67: Τεμπέλη

Πριν από λίγο καιρό δεν ήμασταν ενθουσιασμένοι από την προοπτική να έχουμε την ενσωματωμένη μνήμη σε άλμα μικροϋπολογιστών 8-bit από 8 KB σε 64 KB. Κρίνοντας από τις ολοένα αυξανόμενες, πεινασμένες για πόρους εφαρμογές που χρησιμοποιούμε τώρα, είναι εκπληκτικό το γεγονός ότι κάποιος κατάφερε ποτέ να γράψει ένα πρόγραμμα για να χωρέσει σε αυτήν τη μικρή μνήμη. Ενώ έχουμε πολύ περισσότερη μνήμη για να παίξουμε με αυτές τις μέρες, μερικά πολύτιμα μαθήματα μπορούν να αντληθούν από τις τεχνικές που έχουν καθιερωθεί για να εργαστούν μέσα σε τόσο στενούς περιορισμούς.

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

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

Μία από τις τεχνικές εξοικονόμησης μνήμης που οι προγραμματιστές Java βρίσκουν χρήσιμες είναι τεμπέλης Με τεμπέληδες, ένα πρόγραμμα αποφεύγει τη δημιουργία ορισμένων πόρων έως ότου ο πόρος χρειαστεί για πρώτη φορά - απελευθερώνοντας πολύτιμο χώρο μνήμης. Σε αυτήν την συμβουλή, εξετάζουμε τεμπέλης τεχνικές instantiation στη φόρτωση της κλάσης Java και τη δημιουργία αντικειμένων, καθώς και τις ειδικές εκτιμήσεις που απαιτούνται για τα μοτίβα Singleton. Το υλικό σε αυτήν την άκρη προέρχεται από το έργο στο Κεφάλαιο 9 του βιβλίου μας, Java in Practice: Στυλ σχεδιασμού και ιδιώματα για αποτελεσματική Java (βλέπε πόρους).

Πρόθυμος έναντι τεμπέλης παράσταση: ένα παράδειγμα

Εάν είστε εξοικειωμένοι με το πρόγραμμα περιήγησης Web της Netscape και έχετε χρησιμοποιήσει και τις δύο εκδόσεις 3.x και 4.x, αναμφίβολα έχετε παρατηρήσει μια διαφορά στον τρόπο φόρτωσης του χρόνου εκτέλεσης Java. Εάν κοιτάξετε την οθόνη εκκίνησης κατά την εκκίνηση του Netscape 3, θα παρατηρήσετε ότι φορτώνει διάφορους πόρους, συμπεριλαμβανομένης της Java. Ωστόσο, όταν ξεκινάτε το Netscape 4.x, δεν φορτώνει τον χρόνο εκτέλεσης Java - περιμένει έως ότου επισκεφθείτε μια ιστοσελίδα που περιλαμβάνει την ετικέτα. Αυτές οι δύο προσεγγίσεις απεικονίζουν τις τεχνικές του ανυπομονησία (φορτώστε το σε περίπτωση που χρειαστεί) και τεμπέλης (περιμένετε έως ότου ζητηθεί πριν το φορτώσετε, καθώς μπορεί να μην χρειαστεί ποτέ).

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

Εξετάστε το τεμπέλης παράδειγμα ως πολιτική διατήρησης πόρων

Η τεμπέλης παρουσίαση στην Java εμπίπτει σε δύο κατηγορίες:

  • Οκνηρή φόρτωση τάξης
  • Οκνηρή δημιουργία αντικειμένων

Οκνηρή φόρτωση τάξης

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

MyUtils.classMethod (); // πρώτη κλήση σε μια μέθοδο στατικής κατηγορίας Vector v = new Vector (); // πρώτη κλήση στον χειριστή νέο 

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

Οκνηρή δημιουργία αντικειμένων

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

Για να εισαγάγει την έννοια της τεμπέλης δημιουργίας αντικειμένων, ας ρίξουμε μια ματιά σε ένα απλό παράδειγμα κώδικα όπου a Πλαίσιο χρησιμοποιεί ένα Κουτί μηνυμάτων για εμφάνιση μηνυμάτων σφάλματος:

δημόσια τάξη MyFrame επεκτείνει το πλαίσιο {private MessageBox mb_ = new MessageBox (); // ιδιωτικό βοηθό που χρησιμοποιείται από αυτήν την κατηγορία private void showMessage (String message) {// ορίστε το κείμενο μηνύματος mb_.setMessage (μήνυμα); mb_.pack (); mb_.show (); }} 

Στο παραπάνω παράδειγμα, όταν μια παρουσία του MyFrame δημιουργείται, το Κουτί μηνυμάτων δημιουργείται επίσης η παρουσία mb_. Οι ίδιοι κανόνες ισχύουν αναδρομικά. Έτσι, τυχόν μεταβλητές παρουσίας αρχικοποιήθηκαν ή εκχωρήθηκαν στην τάξη Κουτί μηνυμάτωνΚατασκευαστής διατίθενται επίσης από το σωρό και ούτω καθεξής. Εάν η παρουσία του MyFrame δεν χρησιμοποιείται για την εμφάνιση μηνύματος σφάλματος σε μια περίοδο σύνδεσης, σπαταλάμε τη μνήμη άσκοπα.

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

Εξετάστε το τεμπέλης παράδειγμα ως πολιτική μείωσης των απαιτήσεων πόρων

Η τεμπέλη προσέγγιση στο παραπάνω παράδειγμα παρατίθεται παρακάτω, όπου το αντικείμενο mb_ είναι instantiated στην πρώτη κλήση προς showMessage (). (Δηλαδή, έως ότου πραγματικά απαιτείται από το πρόγραμμα.)

δημόσια τελική τάξη MyFrame επεκτείνει το πλαίσιο {private MessageBox mb_; // null, implicit // ιδιωτικό βοηθό που χρησιμοποιείται από αυτήν την κλάση private void showMessage (String message) {if (mb _ == null) // πρώτη κλήση σε αυτήν τη μέθοδο mb_ = new MessageBox (); // ορίστε το μήνυμα κειμένου mb_.setMessage (μήνυμα); mb_.pack (); mb_.show (); }} 

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

Ένα πραγματικό παράδειγμα

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

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

δημόσια κλάση ImageFile {ιδιωτικό όνομα αρχείου String_; ιδιωτική εικόνα Image_; δημόσιο ImageFile (Όνομα αρχείου συμβολοσειράς) {filename_ = όνομα αρχείου; // φόρτωση της εικόνας} δημόσια συμβολοσειρά getName () {return filename_;} public Image getImage () {return image_; }} 

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

Εδώ είναι το ενημερωμένο Αρχείο εικόνας τάξη χρησιμοποιώντας την ίδια προσέγγιση με την τάξη MyFrame έκανε με το Κουτί μηνυμάτων μεταβλητή παρουσίας:

δημόσια κλάση ImageFile {ιδιωτικό όνομα αρχείου String_; ιδιωτική εικόνα Image_; // = null, implicit public ImageFile (String όνομα αρχείου) {// αποθηκεύστε μόνο το όνομα αρχείου filename_ = όνομα αρχείου; } δημόσια συμβολοσειρά getName () {return filename_;} public Image getImage () {if (image _ == null) {// πρώτη κλήση στο getImage () // φόρτωση της εικόνας ...} επιστροφή εικόνας_; }} 

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

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

Τεμπέλης παρουσίαση για μοτίβα Singleton στην Java

Ας ρίξουμε μια ματιά στο μοτίβο Singleton. Ακολουθεί η γενική φόρμα στην Java:

δημόσια τάξη Singleton {private Singleton () {} static private Singleton instance_ = new Singleton (); στατική δημόσια παρουσία Singleton () {return instance_; } // δημόσιες μέθοδοι} 

Στη γενική έκδοση, δηλώσαμε και αρχικοποιήσαμε το παράδειγμα_ πεδίο ως εξής:

στατικό τελικό Singleton instance_ = νέο Singleton (); 

Αναγνώστες εξοικειωμένοι με την εφαρμογή C ++ του Singleton που γράφτηκε από το GoF (η συμμορία των τεσσάρων που έγραψε το βιβλίο Σχέδια σχεδίασης: Στοιχεία επαναχρησιμοποιήσιμου αντικειμενοστραφούς λογισμικού - Οι Gamma, Helm, Johnson και Vlissides) μπορεί να εκπλαγούν που δεν αναβάλλαμε την αρχικοποίηση του παράδειγμα_ πεδίο μέχρι την κλήση στο παράδειγμα() μέθοδος. Έτσι, χρησιμοποιώντας τεμπέλης

δημόσια στατική παρουσία Singleton () {if (instance _ == null) // Lazy instantiation instance_ = new Singleton (); επιστροφή instance_; } 

Η παραπάνω λίστα είναι μια απευθείας θύρα του παραδείγματος C ++ Singleton που δίνεται από το GoF, και συχνά θεωρείται επίσης ως η γενική έκδοση Java. Εάν είστε ήδη εξοικειωμένοι με αυτήν τη φόρμα και εκπλαγείτε από το γεγονός ότι δεν καταγράψαμε το γενικό μας Singleton έτσι, θα εκπλαγείτε ακόμη περισσότερο όταν μάθετε ότι είναι εντελώς περιττό στην Java! Αυτό είναι ένα κοινό παράδειγμα του τι μπορεί να συμβεί εάν μεταφέρετε κώδικα από τη μία γλώσσα στην άλλη χωρίς να λάβετε υπόψη τα αντίστοιχα περιβάλλοντα χρόνου εκτέλεσης.

Για την εγγραφή, η έκδοση του GoF's C ++ του Singleton χρησιμοποιεί τεμπέληδες, επειδή δεν υπάρχει καμία εγγύηση για τη σειρά στατικής αρχικοποίησης των αντικειμένων κατά το χρόνο εκτέλεσης. (Δείτε το Singleton του Scott Meyer για μια εναλλακτική προσέγγιση στο C ++.) Στην Java, δεν χρειάζεται να ανησυχούμε για αυτά τα ζητήματα.

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

Singleton s = Singleton.instance (); 

Η πρώτη κλήση προς Singleton.instance () σε ένα πρόγραμμα αναγκάζει τον χρόνο εκτέλεσης Java να φορτώσει την τάξη Μοναδικό χαρτί. Ως πεδίο παράδειγμα_ δηλώνεται ως στατικό, ο χρόνος εκτέλεσης της Java θα το προετοιμάσει μετά τη φόρτωση της κλάσης με επιτυχία. Έτσι εγγυάται ότι η κλήση προς Singleton.instance () θα επιστρέψει ένα πλήρως αρχικοποιημένο Singleton - λάβετε τη φωτογραφία;

Τεμπέλης παρουσίαση: επικίνδυνη σε εφαρμογές πολλαπλών νημάτων

Η χρήση τεμπέλης παρουσίας για ένα συγκεκριμένο Singleton δεν είναι μόνο περιττή στην Java, αλλά είναι απολύτως επικίνδυνο στο πλαίσιο εφαρμογών πολλαπλών νημάτων. Σκεφτείτε την τεμπέλη έκδοση του Singleton.instance () μέθοδο, όπου δύο ή περισσότερα ξεχωριστά νήματα προσπαθούν να λάβουν μια αναφορά στο αντικείμενο μέσω παράδειγμα(). Εάν ένα νήμα έχει προκριθεί μετά την επιτυχή εκτέλεση της γραμμής αν (παράδειγμα _ == null), αλλά πριν ολοκληρωθεί η γραμμή instance_ = νέο Singleton (), ένα άλλο νήμα μπορεί επίσης να εισαγάγει αυτήν τη μέθοδο με instance_ still == μηδέν - άσχημο!

Το αποτέλεσμα αυτού του σεναρίου είναι η πιθανότητα να δημιουργηθούν ένα ή περισσότερα αντικείμενα Singleton. Αυτό είναι ένα μεγάλο πονοκέφαλο όταν η τάξη Singleton συνδέεται, για παράδειγμα, με μια βάση δεδομένων ή απομακρυσμένο διακομιστή. Η απλή λύση σε αυτό το πρόβλημα θα ήταν να χρησιμοποιήσετε τη συγχρονισμένη λέξη κλειδί για να προστατεύσετε τη μέθοδο από πολλά νήματα που την εισάγουν ταυτόχρονα:

συγχρονισμένη στατική δημόσια παρουσία () {...} 

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

Το ιδίωμα διπλού ελέγχου

Χρησιμοποιήστε το ιδίωμα διπλού ελέγχου για να προστατέψετε μεθόδους χρησιμοποιώντας τεμπέλης. Δείτε πώς μπορείτε να το εφαρμόσετε στην Java:

δημόσια στατική παρουσία Singleton () {if (instance _ == null) // δεν θέλετε να αποκλείσετε εδώ {// δύο ή περισσότερα νήματα μπορεί να είναι εδώ !!! συγχρονισμένος (Singleton.class) {// πρέπει να ελέγξει ξανά καθώς ένα από τα // αποκλεισμένα νήματα μπορεί ακόμα να εισέλθει εάν (instance _ == null) instance_ = new Singleton (); // safe}} return instance_; } 

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

Η χρήση πολλαπλών νημάτων στην Java μπορεί να είναι πολύ περίπλοκη. Στην πραγματικότητα, το θέμα της ταυτόχρονης συνάφειας είναι τόσο μεγάλο που ο Doug Lea έχει γράψει ένα ολόκληρο βιβλίο σε αυτό: Ταυτόχρονος προγραμματισμός στην Java. Εάν είστε νέοι στον ταυτόχρονο προγραμματισμό, σας συνιστούμε να λάβετε ένα αντίγραφο αυτού του βιβλίου προτού ξεκινήσετε τη σύνταξη σύνθετων συστημάτων Java που βασίζονται σε πολλά νήματα.