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

Πώς να πλοηγηθείτε στο απατηλά απλό μοτίβο Singleton

Το μοτίβο Singleton είναι παραπλανητικά απλό, ακόμη και ειδικά για προγραμματιστές Java. Σε αυτό το κλασικό JavaWorld άρθρο, ο David Geary δείχνει πώς οι προγραμματιστές Java εφαρμόζουν singletons, με παραδείγματα κώδικα για multithreading, classloaders και σειριοποίηση χρησιμοποιώντας το μοτίβο Singleton. Ολοκληρώνει με μια ματιά στην εφαρμογή των singleton μητρώων προκειμένου να καθορίσει singletons στο χρόνο εκτέλεσης.

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

Το σχέδιο σχεδίασης Singleton αντιμετωπίζει όλες αυτές τις ανησυχίες. Με το σχέδιο σχεδίασης Singleton μπορείτε:

  • Βεβαιωθείτε ότι δημιουργείται μόνο μία παρουσία μιας κλάσης
  • Παρέχετε ένα καθολικό σημείο πρόσβασης στο αντικείμενο
  • Να επιτρέπονται πολλές παρουσίες στο μέλλον χωρίς να επηρεάζονται οι πελάτες μιας κατηγορίας singleton

Αν και το μοτίβο σχεδίασης Singleton - όπως αποδεικνύεται παρακάτω από το παρακάτω σχήμα - είναι ένα από τα πιο απλά μοτίβα σχεδίασης, παρουσιάζει μια σειρά από παγίδες για τον απρόσεκτο προγραμματιστή Java. Αυτό το άρθρο ασχολείται με το σχέδιο σχεδίασης Singleton και αντιμετωπίζει αυτές τις παγίδες.

Περισσότερα για τα σχέδια σχεδίασης Java

Μπορείτε να διαβάσετε ολόκληρο τον David Geary's Στήλες Java Design Patternsή δείτε μια λίστα των JavaWorld's πιο πρόσφατα άρθρα σχετικά με τα πρότυπα σχεδίασης Java. Βλέπω "Σχέδια σχεδιασμού, η μεγάλη εικόνα"για μια συζήτηση σχετικά με τα πλεονεκτήματα και τα μειονεκτήματα της χρήσης των συμμοριών των τεσσάρων μοτίβων. Θέλετε περισσότερα; Παραδώστε το ενημερωτικό δελτίο Enterprise Java στα εισερχόμενά σας.

Το μοτίβο Singleton

Σε Σχέδια σχεδίασης: Στοιχεία επαναχρησιμοποιήσιμου αντικειμενοστραφούς λογισμικού, η συμμορία των τεσσάρων περιγράφει το μοτίβο Singleton ως εξής:

Βεβαιωθείτε ότι μια τάξη έχει μόνο μία παρουσία και παρέχει ένα παγκόσμιο σημείο πρόσβασης σε αυτήν.

Το παρακάτω σχήμα απεικονίζει το διάγραμμα τάξης μοτίβου Singleton.

Όπως μπορείτε να δείτε, δεν υπάρχουν πολλά στο σχέδιο του Singleton. Οι Singletons διατηρούν μια στατική αναφορά στη μοναδική παρουσία singleton και επιστρέφουν μια αναφορά σε αυτήν την παρουσία από μια στατική παράδειγμα() μέθοδος.

Το παράδειγμα 1 δείχνει μια κλασική εφαρμογή μοτίβου σχεδίασης Singleton:

Παράδειγμα 1. Το κλασικό singleton

δημόσια κλάση ClassicSingleton {ιδιωτικό στατικό ClassicSingleton instance = null; προστατευμένο ClassicSingleton () {// Υπάρχει μόνο για να νικήσει το instantiation. } δημόσιο στατικό ClassicSingleton getInstance () {if (instance == null) {instance = νέο ClassicSingleton (); } εμφάνιση επιστροφής; }}

Το singleton που εφαρμόζεται στο Παράδειγμα 1 είναι εύκολο να γίνει κατανοητό. ο ClassicSingleton Η κλάση διατηρεί μια στατική αναφορά στην παρουσία μόνου singleton και επιστρέφει αυτήν την αναφορά από το στατικό getInstance () μέθοδος.

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

Δεύτερον, παρατηρήστε ότι ClassicSingleton εφαρμόζει έναν προστατευμένο κατασκευαστή, έτσι ώστε οι πελάτες να μην μπορούν να εμφανιστούν ClassicSingleton περιπτώσεις; Ωστόσο, μπορεί να εκπλαγείτε όταν ανακαλύπτετε ότι ο ακόλουθος κωδικός είναι απολύτως νόμιμος:

δημόσια τάξη SingletonInstantiator {public SingletonInstantiator () {ClassicSingleton instance = ClassicSingleton.getInstance (); ClassicSingleton anotherInstance =νέο ClassicSingleton (); ... } }

Πώς μπορεί η κλάση στο προηγούμενο τμήμα κώδικα - που δεν επεκτείνεται ClassicSingleton-δημιουργώ ένα ClassicSingleton περίπτωση εάν το ClassicSingleton ο κατασκευαστής προστατεύεται; Η απάντηση είναι ότι οι προστατευμένοι κατασκευαστές μπορούν να κληθούν από υποκατηγορίες και από άλλες τάξεις στο ίδιο πακέτο. Επειδή ClassicSingleton και SingletonInstantiator βρίσκονται στο ίδιο πακέτο (το προεπιλεγμένο πακέτο), SingletonInstantiator () μέθοδοι μπορούν να δημιουργήσουν ClassicSingleton περιπτώσεις. Αυτό το δίλημμα έχει δύο λύσεις: Μπορείτε να κάνετε το ClassicSingleton κατασκευαστή ιδιωτικό έτσι ώστε μόνο ClassicSingleton () οι μέθοδοι το αποκαλούν? Ωστόσο, αυτό σημαίνει ClassicSingleton δεν μπορεί να υποκατηγορηθεί. Μερικές φορές, αυτή είναι μια επιθυμητή λύση. Εάν ναι, είναι καλή ιδέα να δηλώσετε την τάξη σας στο singleton τελικός, που καθιστά αυτή την πρόθεση σαφή και επιτρέπει στον μεταγλωττιστή να εφαρμόζει βελτιστοποιήσεις απόδοσης. Η άλλη λύση είναι να τοποθετήσετε την τάξη singleton σας σε ένα σαφές πακέτο, έτσι οι τάξεις σε άλλα πακέτα (συμπεριλαμβανομένου του προεπιλεγμένου πακέτου) δεν μπορούν να δημιουργήσουν στιγμιότυπα παρουσιών singleton.

Ένα τρίτο ενδιαφέρον σημείο για ClassicSingleton: είναι πιθανό να υπάρχουν πολλές παρουσίες singleton εάν οι τάξεις που φορτώνονται από διαφορετικούς classloaders έχουν πρόσβαση σε ένα singleton. Αυτό το σενάριο δεν είναι τόσο παραπλανητικό. Για παράδειγμα, ορισμένα servlet container περιέχουν ξεχωριστούς classloaders για κάθε servlet, οπότε αν δύο servlets έχουν πρόσβαση σε ένα singleton, ο καθένας θα έχει τη δική του παρουσία.

Τέταρτον, εάν ClassicSingleton εφαρμόζει το java.io.Serializable διεπαφή, οι παρουσίες της τάξης μπορούν να σειριοποιηθούν και να αποστειρωθούν. Ωστόσο, εάν πραγματοποιήσετε σειριοποίηση ενός αντικειμένου singleton και στη συνέχεια αποεπιτυλίσετε αυτό το αντικείμενο περισσότερες από μία φορές, θα έχετε πολλές παρουσίες singleton.

Τέλος, και ίσως το πιο σημαντικό, το Παράδειγμα 1 ClassicSingleton η τάξη δεν είναι ασφαλής για νήματα. Εάν δύο νήματα — θα τα ονομάσουμε Νήμα 1 και Νήμα 2 — καλέστε ClassicSingleton.getInstance () ταυτόχρονα, δύο ClassicSingleton Οι παρουσίες μπορούν να δημιουργηθούν εάν το νήμα 1 έχει προκριθεί αμέσως μόλις εισέλθει στο αν Το μπλοκ και ο έλεγχος στη συνέχεια δίνεται στο νήμα 2.

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

Δοκιμή μονότολων

Σε όλο το υπόλοιπο αυτού του άρθρου, χρησιμοποιώ το JUnit σε συνεννόηση με το log4j για να δοκιμάσω μαθήματα singleton. Εάν δεν είστε εξοικειωμένοι με το JUnit ή το log4j, ανατρέξτε στην ενότητα Πόροι.

Το Παράδειγμα 2 παραθέτει μια υπόθεση δοκιμής JUnit που δοκιμάζει το singleton του Παραδείγματος 1:

Παράδειγμα 2. Μια δοκιμαστική περίπτωση

εισαγωγή org.apache.log4j.Logger; εισαγωγή junit.framework.Assert; εισαγωγή junit.framework.TestCase; δημόσια τάξη Το SingletonTest επεκτείνει το TestCase {private ClassicSingleton sone = null, stwo = null; ιδιωτικό στατικό Logger logger = Logger.getRootLogger (); δημόσιο SingletonTest (όνομα συμβολοσειράς) {super (name); } public void setUp () {logger.info ("λήψη singleton ..."); sone = ClassicSingleton.getInstance (); logger.info ("... πήρα singleton:" + sone); logger.info ("λήψη singleton ..."); stwo = ClassicSingleton.getInstance (); logger.info ("... πήρα singleton:" + stwo); } public void testUnique () {logger.info ("έλεγχος για την ισότητα"); Assert.assertEquals (true, sone == stwo); }}

Επικαλείται τη δοκιμαστική θήκη του παραδείγματος 2 ClassicSingleton.getInstance () δύο φορές και αποθηκεύει τις επιστρεφόμενες αναφορές σε μεταβλητές μελών. ο testUnique () Η μέθοδος ελέγχει για να δει ότι οι αναφορές είναι ίδιες. Το παράδειγμα 3 δείχνει ότι η έξοδος δοκιμαστικής θήκης:

Παράδειγμα 3. Έξοδος δοκιμαστικής θήκης

Buildfile: build.xml init: [echo] Build 20030414 (14-04-2003 03:08) compile: run-test-text: [java]. Κύριο INFO: παίρνω singleton... [java] INFO main: δημιούργησε singleton: Singleton @ e86f41 [java] INFO main: ... got singleton: Singleton @ e86f41 [java] INFO main: παίρνω singleton... [java] INFO main: ... got singleton: Singleton @ e86f41 [java] INFO main: έλεγχος singletons για ισότητα [java] Ώρα: 0,032 [java] ΟΚ (1 δοκιμή)

Όπως απεικονίζει η προηγούμενη λίστα, η απλή δοκιμή του Παραδείγματος 2 περνά με ιπτάμενα χρώματα - οι δύο αναφορές μεμονωμένα που αποκτήθηκαν με ClassicSingleton.getInstance () είναι πράγματι πανομοιότυπα · Ωστόσο, αυτές οι αναφορές ελήφθησαν σε ένα μόνο νήμα. Η επόμενη ενότητα δοκιμάζει την τάξη των singleton με πολλά νήματα.

Θέματα πολλαπλών νημάτων

Παράδειγμα 1 ClassicSingleton.getInstance () Η μέθοδος δεν είναι ασφαλής για το νήμα λόγω του ακόλουθου κώδικα:

1: if (instance == null) {2: instance = νέο Singleton (); 3:}

Εάν ένα νήμα έχει προκριθεί στη Γραμμή 2 πριν γίνει η ανάθεση, το παράδειγμα η μεταβλητή μέλους θα παραμείνει μηδενικό, και ένα άλλο νήμα μπορεί στη συνέχεια να εισέλθει στο αν ΟΙΚΟΔΟΜΙΚΟ ΤΕΤΡΑΓΩΝΟ. Σε αυτήν την περίπτωση, θα δημιουργηθούν δύο ξεχωριστές παρουσίες. Δυστυχώς, αυτό το σενάριο σπάνια εμφανίζεται και είναι επομένως δύσκολο να δημιουργηθεί κατά τη διάρκεια των δοκιμών. Για να επεξηγήσω αυτό το νήμα Ρωσική ρουλέτα, έχω αναγκάσει το ζήτημα με την εκ νέου εφαρμογή της τάξης του Παραδείγματος 1 Το παράδειγμα 4 δείχνει την αναθεωρημένη τάξη singleton:

Παράδειγμα 4. Στοίβα το κατάστρωμα

εισαγωγή org.apache.log4j.Logger; δημόσια τάξη Singleton {ιδιωτικό στατικό Singleton singleton = null; ιδιωτικό στατικό Logger logger = Logger.getRootLogger (); ιδιωτικό στατικό boolean πρώτο νήμα = αλήθεια; προστατευμένο Singleton () {// Υπάρχει μόνο για να νικήσει την παρουσία. } δημόσιο στατικό Singleton getInstance () { if (singleton == null) {simulateRandomActivity (); singleton = νέο Singleton (); } logger.info ("δημιουργήθηκε singleton:" + singleton); επιστροφή singleton; } ιδιωτικό στατικό κενό simulateRandomActivity() { δοκιμάστε { εάν (firstThread) {firstThread = false; logger.info ("κοιμάται ..."); // Αυτός ο υπνάκο θα δώσει αρκετό χρόνο στο δεύτερο νήμα // για να φτάσετε στο πρώτο νήμα.Thread.currentThread (). Ύπνος (50); }} catch (InterruptException ex) {logger.warn ("Διακοπή ύπνου"); }}}

Το singleton του παραδείγματος 4 μοιάζει με την κλάση του παραδείγματος 1, εκτός από το singleton στην προηγούμενη λίστα στοίβες στο κατάστρωμα για να επιβάλει ένα σφάλμα πολλαπλών νημάτων. Την πρώτη φορά το getInstance () ονομάζεται μέθοδος, το νήμα που επικαλέστηκε τη μέθοδο κοιμάται για 50 χιλιοστά του δευτερολέπτου, το οποίο δίνει σε άλλο νήμα χρόνο να καλέσει getInstance () και δημιουργήστε μια νέα παρουσία singleton. Όταν το νήμα ύπνου ξυπνά, δημιουργεί επίσης μια νέα παρουσία singleton και έχουμε δύο παρουσίες singleton. Αν και η τάξη του Παραδείγματος 4 είναι σχεδιασμένη, διεγείρει την πραγματική κατάσταση όπου το πρώτο νήμα που καλεί getInstance () παίρνει προτίμηση.

Παράδειγμα 5 δοκιμές Το σινγκλ του παραδείγματος 4:

Παράδειγμα 5. Ένα τεστ που αποτυγχάνει

εισαγωγή org.apache.log4j.Logger; εισαγωγή junit.framework.Assert; εισαγωγή junit.framework.TestCase; δημόσια τάξη SingletonTest επεκτείνει το TestCase {private static Logger logger = Logger.getRootLogger (); ιδιωτικό στατικό Singleton μοναδικό χαρτί = μηδέν; δημόσιο SingletonTest (όνομα συμβολοσειράς) {super (name); } δημόσιο άκυρο setUp () { singleton = null; } public void testUnique () ρίχνει το InterruptException {// Και τα δύο νήματα καλούν Singleton.getInstance (). Thread threadOne = νέο νήμα (νέο SingletonTestRunnable ()), threadTwo = νέο νήμα (νέο SingletonTestRunnable ()); threadOne.start ();νήμαTwo.start (); νήμαOne.join (); νήμαTwo.join (); } ιδιωτική στατική τάξη SingletonTestRunnable υλοποιεί Runnable {public void run () {// Λάβετε μια αναφορά στο singleton. Singleton s = Singleton.getInstance (); // Προστατέψτε τη μεταβλητή μέλους singleton από την πρόσβαση πολλαπλών νημάτων. συγχρονισμένη (SingletonTest.class) {if (singleton == null) // Εάν η τοπική αναφορά είναι μηδενική ... singleton = s; // ... ορίστε το στο singleton} // Η τοπική αναφορά πρέπει να είναι ίση με τη μία και // μόνο παρουσία του Singleton. Διαφορετικά, έχουμε δύο // παρουσίες Singleton. Assert.assertEquals (true, s == singleton); } } }

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

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