טיפ ג'אווה 67: מיישר עצלן

לא עבר כל כך הרבה זמן עד שהתרגשנו מהאפשרות לקבל את הזיכרון על הלוח בקפיצה ממיקרו-מחשב של 8 סיביות מ -8 KB ל -64 KB. אם לשפוט לפי היישומים ההולכים וגדלים, הרעבים משאבים שאנו משתמשים כעת, מדהים שמישהו הצליח לכתוב תוכנית שתתאים לכמות הזיכרון הזעירה הזו. אמנם יש לנו הרבה יותר זיכרון לשחק איתו בימים אלה, אך ניתן ללמוד כמה לקחים חשובים מהטכניקות שנקבעו כדי לעבוד באילוצים כה הדוקים.

יתר על כן, תכנות Java אינו נוגע רק לכתיבת יישומונים ויישומים לפריסה במחשבים אישיים ובתחנות עבודה; ג'אווה עשתה כניסה חזקה גם לשוק המערכות המשובצות. למערכות משובצות נוכחיות יש מעט משאבי זיכרון יחסית וכוח מחשוב, ולכן רבים מהנושאים הישנים העומדים בפני מתכנתים צצו מחדש עבור מפתחי ג'אווה העובדים בתחום המכשירים.

איזון בין גורמים אלה הוא בעיה עיצובית מרתקת: חשוב לקבל את העובדה שאף פיתרון בתחום העיצוב המוטבע לא יהיה מושלם. לכן עלינו להבין את סוגי הטכניקות שיהיו שימושיות להשגת האיזון הדק הנדרש לעבודה במגבלות פלטפורמת הפריסה.

אחת הטכניקות לשמירת זיכרון מתכנתים שג'אווה מועילות בהן היא מיידיות עצלה. עם אינסטנטציה עצלה, תוכנית נמנעת מיצירת משאבים מסוימים עד שיידרש לראשונה המשאב - מה שמפנה מקום זיכרון יקר. בטיפ זה, אנו בוחנים טכניקות מיידיות עצלות בטעינה בכיתת Java ויצירת אובייקטים, ואת השיקולים המיוחדים הנדרשים לדפוסי סינגלטון. החומר בטיפ זה נובע מהעבודה בפרק 9 בספרנו, Java in Practice: Design Styles & Idioms for Effective Java (ראה משאבים).

אינסטינציה נלהבת לעומת עצלה: דוגמה

אם אתה מכיר את דפדפן האינטרנט של Netscape והשתמשת בשתי הגרסאות 3.x ו- 4.x, ללא ספק שמת לב להבדל באופן טעינת זמן הריצה של Java. אם אתה מסתכל על מסך הפתיחה כאשר Netscape 3 מופעל, תשים לב שהוא טוען משאבים שונים, כולל Java. עם זאת, כאשר אתה מפעיל את Netscape 4.x, הוא אינו טוען את זמן הריצה של Java - הוא ממתין עד שתבקר בדף אינטרנט הכולל את התג. שתי גישות אלה ממחישות את הטכניקות של ייצוב מיידי ( טען אותו למקרה שהוא נדרש) ואינסטנטציה עצלה (המתן עד שתתבקש לפני שתטען אותו, מכיוון שלעולם לא יהיה צורך בכך).

יש חסרונות בשתי הגישות: מצד אחד, טעינת משאב תמיד מבזבזת זיכרון יקר אם לא משתמשים במשאב במהלך אותה הפעלה; מצד שני, אם הוא לא נטען, אתה משלם את המחיר במונחים של זמן טעינה כאשר המשאב נדרש לראשונה.

שקול אינסטינציה עצלה כמדיניות לשימור משאבים

מיידיות עצלה בג'אווה מתחלקת לשתי קטגוריות:

  • העמסת כיתה עצלה
  • יצירת עצמים עצלה

העמסת כיתה עצלה

זמן הריצה של ג'אווה כולל מייצב עצלן מובנה לשיעורים. הכיתות נטענות בזיכרון רק כאשר הם מופנים לראשונה. (הם עשויים להיות נטענים משרת אינטרנט באמצעות HTTP תחילה.)

MyUtils.classMethod (); // קריאה ראשונה לשיטת מחלקה סטטית וקטור v = וקטור חדש (); // השיחה הראשונה למפעיל חדשה

טעינה של מחלקה עצלה היא תכונה חשובה בסביבת זמן הריצה של Java מכיוון שהיא יכולה להפחית את השימוש בזיכרון בנסיבות מסוימות. לדוגמא, אם חלק מתוכנית לעולם אינו מבוצע במהלך הפעלה, שיעורים המוזכרים רק בחלק זה של התוכנית לעולם לא ייטענו.

יצירת עצמים עצלה

יצירת עצמים עצלנים משולבת היטב לטעינה של מעמד עצלן. בפעם הראשונה שאתה משתמש במילת המפתח החדשה בסוג מחלקה שלא נטען בעבר, זמן הריצה של Java יטען אותה עבורך. יצירת עצמים עצלנים יכולה להפחית את השימוש בזיכרון במידה רבה בהרבה מהעומס בכיתה עצלה.

כדי להציג את הרעיון של יצירת עצמים עצלים, בואו נסתכל על דוגמת קוד פשוטה שבה a Frameמשתמש a MessageBoxכדי להציג הודעות שגיאה:

מחלקה ציבורית MyFrame מרחיבה את מסגרת {MessageBox פרטי mb_ = MessageBox חדש (); // עוזר פרטי המשמש את המעמד הריק הפרטי showMessage (הודעת מחרוזת) בכיתה זו {// הגדר את טקסט ההודעה mb_.setMessage (הודעה); mb_.pack (); mb_.show (); }}

בדוגמה שלעיל, כאשר נוצר מופע של MyFrame, נוצר גם MessageBoxהמופע mb_. אותם כללים חלים רקורסיבית. כך שכל משתני מופע שאותחל או הוקצו בבנאי הכיתה MessageBoxמוקצים גם מהערמה וכן הלאה. אם המופע של MyFrameאינו משמש להצגת הודעת שגיאה בפגישה, אנו מבזבזים זיכרון שלא לצורך.

בדוגמה די פשוטה זו, אנחנו לא באמת נרוויח יותר מדי. אבל אם אתה מחשיב מחלקה מורכבת יותר, המשתמשת בכיתות רבות אחרות, אשר בתורן משתמשות ומייצרות יותר אובייקטים באופן רקורסיבי, השימוש בזיכרון הפוטנציאלי ברור יותר.

שקול מיידיות עצלה כמדיניות להפחתת דרישות המשאבים

הגישה העצלנית לדוגמה שלמעלה מופיעה להלן, שם object mb_מיידית בשיחה הראשונה showMessage(). (כלומר, לא עד שהתוכנית זקוקה לכך בפועל).

מחלקת הגמר הציבורית MyFrame מרחיבה את המסגרת {MessageBox פרטי mb_; // null, implicit // עוזר פרטי המשמש את המעמד הריק הזה showmessage (הודעת מחרוזת) בכיתה זו {if (mb _ == null) // התקשר ראשון לשיטה זו mb_ = MessageBox חדש (); // הגדר את טקסט ההודעה mb_.setMessage (הודעה); mb_.pack (); mb_.show (); }}

אם תסתכל מקרוב showMessage(), תראה שקודם נקבע אם משתנה המופע mb_ שווה לאפס. מכיוון שלא אותנו את mb_ בנקודת ההכרזה, זמן הריצה של ג'אווה דאג לכך עבורנו. לפיכך, אנו יכולים להמשיך בבטחה על ידי יצירת MessageBoxהמופע. כל השיחות העתידיות אליו showMessage()ימצאו ש- mb_ אינו שווה ל- null, ולכן מדלג על יצירת האובייקט ומשתמש במופע הקיים.

דוגמה בעולם האמיתי

בואו נבחן דוגמה מציאותית יותר, שבה מייצב עצלן יכול למלא תפקיד מפתח בהפחתת כמות המשאבים המשמשים תוכנית.

נניח שהתבקשנו על ידי לקוח לכתוב מערכת שתאפשר למשתמשים לקטלג תמונות במערכת קבצים ולספק את האפשרות להציג תמונות ממוזערות או תמונות מלאות. הניסיון הראשון שלנו יכול להיות לכתוב כיתה שמטעינה את התמונה בבנאי שלה.

מחלקה ציבורית ImageFile {שם פרטי קובץ מחרוזת_; תמונה תמונה פרטית_; ImageFile ציבורי (שם קובץ מחרוזת) {filename_ = שם קובץ; // טען את התמונה} ציבורי מחרוזת getName () {להחזיר filename_;} תמונה ציבורית ציבורית getImage () {return image_; }}

בדוגמה שלעיל, ImageFileמיישם גישה יותר מדי לייצוב Imageהאובייקט. לטובתו, עיצוב זה מבטיח שתמונה תהיה זמינה מיד בזמן הקריאה ל- getImage(). עם זאת, לא רק שהדבר יכול להיות איטי עד כאב (במקרה של ספריה המכילה תמונות רבות), אלא שתכנון זה יכול למצות את הזיכרון הזמין. כדי להימנע מבעיות פוטנציאליות אלה, אנו יכולים להחליף את היתרונות הביצועיים של גישה מיידית לצורך שימוש מופחת בזיכרון. כפי שאולי ניחשתם, אנו יכולים להשיג זאת באמצעות אינסטנטציה עצלה.

הנה ImageFileהמחלקה המעודכנת המשתמשת באותה גישה כמו המחלקה MyFrameעם MessageBoxמשתנה המופע שלה :

מחלקה ציבורית ImageFile {שם פרטי קובץ מחרוזת_; תמונה תמונה פרטית_; // = null, ImageFile ציבורי מרומז (שם קובץ מחרוזת) {// אחסן רק את שם הקובץ filename_ = שם הקובץ; } public String getName () {return filename_;} image public image getImage () {if (image _ == null) {// call first to getImage () // load the image ...} return image_; }}

בגרסה זו, התמונה בפועל נטענת רק בשיחה הראשונה אל getImage(). אם לסיכום, הפיתרון כאן הוא שכדי להפחית את זמני השימוש בזיכרון ואת זמני ההפעלה, אנו משלמים את מחיר טעינת התמונה בפעם הראשונה שהיא מתבקשת - הכנסת להיט ביצועים באותה נקודה בביצוע התוכנית. זהו ניב נוסף המשקף את Proxyהתבנית בהקשר הדורש שימוש מוגבל בזיכרון.

מדיניות האינסטינציה העצלה שהודגמה לעיל היא בסדר דוגמאות שלנו, אך בהמשך תראה כיצד העיצוב צריך להשתנות בהקשר של מספר נושאים.

מיישר עצלן לדפוסי סינגלטון בג'אווה

בואו נסתכל כעת על דפוס הסינגלטון. הנה הטופס הגנרי ב- Java:

מחלקה ציבורית סינגלטון {פרטי סינגלטון () {} מופע פרטי סינגלטון פרטי_ = סינגלטון חדש (); מופע סטטי ציבורי של סינגלטון () {חזרה מופע_; } // שיטות ציבוריות}

בגרסה הגנרית הכרזנו ואותחלנו את instance_השדה באופן הבא:

גמר סטטי סינגלטון גמר_ = סינגלטון חדש (); 

Readers familiar with the C++ implementation of Singleton written by the GoF (the Gang of Four who wrote the book Design Patterns: Elements of Reusable Object-Oriented Software -- Gamma, Helm, Johnson, and Vlissides) may be surprised that we didn't defer the initialization of the instance_ field until the call to the instance() method. Thus, using lazy instantiation:

public static Singleton instance() { if(instance_==null) //Lazy instantiation instance_= new Singleton(); return instance_; } 

The listing above is a direct port of the C++ Singleton example given by the GoF, and frequently is touted as the generic Java version too. If you already are familiar with this form and were surprised that we didn't list our generic Singleton like this, you'll be even more surprised to learn that it is totally unnecessary in Java! This is a common example of what can occur if you port code from one language to another without considering the respective runtime environments.

For the record, the GoF's C++ version of Singleton uses lazy instantiation because there is no guarantee of the order of static initialization of objects at runtime. (See Scott Meyer's Singleton for an alternative approach in C++ .) In Java, we don't have to worry about these issues.

The lazy approach to instantiating a Singleton is unnecessary in Java because of the way in which the Java runtime handles class loading and static instance variable initialization. Previously, we have described how and when classes get loaded. A class with only public static methods gets loaded by the Java runtime on the first call to one of these methods; which in the case of our Singleton is

Singleton s=Singleton.instance(); 

The first call to Singleton.instance() in a program forces the Java runtime to load the class Singleton. As the field instance_ is declared as static, the Java runtime will initialize it after successfully loading the class. Thus guarantees that the call to Singleton.instance() will return a fully initialized Singleton -- get the picture?

Lazy instantiation: dangerous in multithreaded applications

Using lazy instantiation for a concrete Singleton is not only unnecessary in Java, it's downright dangerous in the context of multithreaded applications. Consider the lazy version of the Singleton.instance() method, where two or more separate threads are attempting to obtain a reference to the object via instance(). If one thread is preempted after successfully executing the line if(instance_==null), but before it has completed the line instance_=new Singleton(), another thread can also enter this method with instance_ still ==null -- nasty!

The outcome of this scenario is the likelihood that one or more Singleton objects will be created. This is a major headache when your Singleton class is, say, connecting to a database or remote server. The simple solution to this problem would be to use the synchronized key word to protect the method from multiple threads entering it at the same time:

synchronized static public instance() {...} 

However, this approach is a bit heavy-handed for most multithreaded applications using a Singleton class extensively, thereby causing blocking on concurrent calls to instance(). By the way, invoking a synchronized method is always much slower than invoking a nonsynchronized one. So what we need is a strategy for synchronization that doesn't cause unnecessary blocking. Fortunately, such a strategy exists. It is known as the double-check idiom.

The double-check idiom

Use the double-check idiom to protect methods using lazy instantiation. Here's how to implement it in Java:

public static Singleton instance() { if(instance_==null) //don't want to block here { //two or more threads might be here!!! synchronized(Singleton.class) { //must check again as one of the //blocked threads can still enter if(instance_==null) instance_= new Singleton();//safe } } return instance_; } 

The double-check idiom improves performance by using synchronization only if multiple threads call instance() before the Singleton is constructed. Once the object has been instantiated, instance_ is no longer ==null, allowing the method to avoid blocking concurrent callers.

שימוש במספר שרשורים ב- Java יכול להיות מורכב מאוד. למעשה, נושא המקבילות כה נרחב עד שדאג לי כתב עליו ספר שלם: תכנות מקביל בג'אווה. אם אתה חדש בתכנות במקביל, אנו ממליצים לך לקבל עותק של ספר זה לפני שתתחיל לכתוב מערכות Java מורכבות המסתמכות על מספר נושאים.