วิธีนำทางรูปแบบ Singleton ที่เรียบง่ายหลอกลวง

รูปแบบ Singleton นั้นเรียบง่ายโดยเฉพาะอย่างยิ่งสำหรับนักพัฒนา Java ในบทความ JavaWorld แบบคลาสสิกนี้ David Geary แสดงให้เห็นว่านักพัฒนา Java ใช้ singletons อย่างไรโดยมีตัวอย่างโค้ดสำหรับมัลติเธรดคลาสโหลดเดอร์และการจัดลำดับโดยใช้รูปแบบ Singleton เขาสรุปด้วยการดูการใช้งานการลงทะเบียนแบบซิงเกิลตันเพื่อระบุ singletons ที่รันไทม์

บางครั้งมันก็เหมาะสมที่จะมีหนึ่งอินสแตนซ์ของคลาส: ตัวจัดการหน้าต่างตัวจัดคิวงานพิมพ์และระบบไฟล์เป็นตัวอย่างต้นแบบ โดยปกติแล้วประเภทของอ็อบเจ็กต์เหล่านั้นหรือที่เรียกว่า singletons จะเข้าถึงได้โดยอ็อบเจ็กต์ที่แตกต่างกันในระบบซอฟต์แวร์ดังนั้นจึงต้องมีการเข้าถึงทั่วโลก แน่นอนว่าเมื่อคุณมั่นใจว่าคุณจะไม่ต้องการมากกว่าหนึ่งอินสแตนซ์ก็เป็นการดีที่คุณจะเปลี่ยนใจ

รูปแบบการออกแบบ Singleton ช่วยแก้ปัญหาเหล่านี้ทั้งหมด ด้วยรูปแบบการออกแบบ Singleton คุณสามารถ:

  • ตรวจสอบให้แน่ใจว่ามีการสร้างคลาสเพียงอินสแตนซ์เดียว
  • ระบุจุดเข้าถึงทั่วโลกไปยังวัตถุ
  • อนุญาตหลายอินสแตนซ์ในอนาคตโดยไม่ส่งผลกระทบต่อไคลเอ็นต์ของคลาสซิงเกิลตัน

แม้ว่ารูปแบบการออกแบบ Singleton ตามหลักฐานด้านล่างในรูปด้านล่างจะเป็นหนึ่งในรูปแบบการออกแบบที่เรียบง่ายที่สุด แต่ก็มีข้อผิดพลาดหลายประการสำหรับนักพัฒนา Java ที่ไม่ระมัดระวัง บทความนี้กล่าวถึงรูปแบบการออกแบบ Singleton และจัดการกับข้อผิดพลาดเหล่านั้น

เพิ่มเติมเกี่ยวกับรูปแบบการออกแบบ Java

คุณสามารถอ่านคอลัมน์ Java Design Patternsทั้งหมดของ David Geary หรือดูรายการบทความล่าสุดของ JavaWorld เกี่ยวกับรูปแบบการออกแบบ Java ดู " รูปแบบการออกแบบภาพรวม " สำหรับการสนทนาเกี่ยวกับข้อดีข้อเสียของการใช้รูปแบบ Gang of Four ต้องการมากขึ้น? รับจดหมายข่าว Enterprise Java ที่ส่งไปยังกล่องจดหมายของคุณ

รูปแบบ Singleton

ในรูปแบบการออกแบบ: องค์ประกอบของซอฟต์แวร์เชิงวัตถุที่นำกลับมาใช้ใหม่ได้ Gang of Four อธิบายถึงรูปแบบ Singleton ดังนี้:

ตรวจสอบให้แน่ใจว่าชั้นเรียนมีอินสแตนซ์เดียวและระบุจุดเข้าถึงทั่วโลก

รูปด้านล่างแสดงแผนภาพคลาสรูปแบบการออกแบบ Singleton

อย่างที่คุณเห็นรูปแบบการออกแบบ Singleton มีไม่มากนัก Singletons รักษาการอ้างอิงแบบคงที่ไปยังอินสแตนซ์ซิงเกิลตัน แต่เพียงผู้เดียวและส่งคืนการอ้างอิงไปยังอินสแตนซ์นั้นจากinstance()วิธีการแบบคงที่

ตัวอย่างที่ 1 แสดงรูปแบบการออกแบบ Singleton แบบคลาสสิก:

ตัวอย่าง 1. ซิงเกิลคลาสสิก

public class ClassicSingleton { private static ClassicSingleton instance = null; protected ClassicSingleton() { // Exists only to defeat instantiation. } public static ClassicSingleton getInstance() { if(instance == null) { instance = new ClassicSingleton(); } return instance; } }

ซิงเกิลตันที่ใช้ในตัวอย่างที่ 1 นั้นเข้าใจง่าย ClassicSingletonระดับรักษาอ้างอิงคงที่เช่นเดียวเดี่ยวและผลตอบแทนที่อ้างอิงจากแบบคงที่getInstance()วิธีการ

มีประเด็นที่น่าสนใจหลายประการเกี่ยวกับClassicSingletonชั้นเรียน ขั้นแรกClassicSingletonใช้เทคนิคที่เรียกว่าการสร้างอินสแตนซ์แบบขี้เกียจเพื่อสร้างซิงเกิลตัน ด้วยเหตุนี้จึงไม่มีการสร้างอินสแตนซ์ซิงเกิลตันจนกว่าจะมีgetInstance()การเรียกใช้เมธอดในครั้งแรก เทคนิคนี้ช่วยให้มั่นใจได้ว่าอินสแตนซ์ซิงเกิลตันถูกสร้างขึ้นเมื่อจำเป็นเท่านั้น

ประการที่สองสังเกตว่าClassicSingletonใช้ตัวสร้างที่ได้รับการป้องกันดังนั้นไคลเอนต์จึงไม่สามารถสร้างClassicSingletonอินสแตนซ์อินสแตนซ์ได้ อย่างไรก็ตามคุณอาจประหลาดใจที่พบว่ารหัสต่อไปนี้ถูกต้องตามกฎหมาย:

public class SingletonInstantiator { public SingletonInstantiator() { ClassicSingleton instance = ClassicSingleton.getInstance(); ClassicSingleton anotherInstance =new ClassicSingleton(); ... } }

คลาสในส่วนโค้ดก่อนหน้าซึ่งไม่ขยายClassicSingleton- สร้างClassicSingletonอินสแตนซ์ได้อย่างไรหากตัวClassicSingletonสร้างได้รับการป้องกัน คำตอบก็คือการป้องกันการก่อสร้างสามารถเรียกโดย subclasses และโดยชั้นเรียนอื่น ๆ ในแพคเกจเดียวกันเนื่องจากClassicSingletonและSingletonInstantiatorอยู่ในแพ็คเกจเดียวกัน (แพ็กเกจเริ่มต้น) จึงSingletonInstantiator()สามารถสร้างClassicSingletonอินสแตนซ์ได้ ภาวะที่กลืนไม่เข้าคายไม่ออกนี้มีสองวิธี: คุณสามารถทำให้ตัวClassicSingletonสร้างเป็นแบบส่วนตัวเพื่อให้เฉพาะClassicSingleton()เมธอดเท่านั้นอย่างไรก็ตามนั่นหมายความว่าClassicSingletonไม่สามารถแบ่งย่อยได้ บางครั้งนั่นเป็นทางออกที่พึงปรารถนา ถ้าเป็นเช่นนั้นคุณควรประกาศคลาสซิงเกิลตันของคุณfinalซึ่งทำให้เจตนานั้นชัดเจนและอนุญาตให้คอมไพเลอร์ใช้การปรับแต่งประสิทธิภาพได้ วิธีแก้ปัญหาอื่นคือการใส่คลาส singleton ของคุณในแพ็กเกจที่ชัดเจนดังนั้นคลาสในแพ็กเกจอื่น ๆ (รวมถึงแพ็กเกจเริ่มต้น) จึงไม่สามารถสร้างอินสแตนซ์ซิงเกิลตันได้

ประเด็นที่สามที่น่าสนใจเกี่ยวกับClassicSingleton: เป็นไปได้ที่จะมีอินสแตนซ์ซิงเกิลตันหลายรายการหากคลาสที่โหลดโดยคลาสโหลดเดอร์ที่แตกต่างกันเข้าถึงซิงเกิลตัน สถานการณ์นั้นไม่ได้เป็นเรื่องไกลตัว ตัวอย่างเช่นคอนเทนเนอร์ servlet บางตัวใช้ classloaders ที่แตกต่างกันสำหรับแต่ละ servlet ดังนั้นหากสอง servlet เข้าถึง singleton แต่ละรายการจะมีอินสแตนซ์ของตัวเอง

ประการที่สี่หากClassicSingletonใช้java.io.Serializableอินเทอร์เฟซอินสแตนซ์ของคลาสสามารถต่ออนุกรมและ deserialized ได้ อย่างไรก็ตามหากคุณทำให้เป็นอนุกรมของวัตถุแบบซิงเกิลตันและต่อจากนั้นจึงยกเลิกการเชื่อมต่อวัตถุนั้นมากกว่าหนึ่งครั้งคุณจะมีอินสแตนซ์ซิงเกิลตันหลายรายการ

สุดท้ายและอาจสำคัญที่สุดClassicSingletonคลาสของตัวอย่างที่ 1 ไม่ปลอดภัยต่อเธรด ถ้าเธรดสองเธรด - เราจะเรียกมันว่าเธรด 1 และเธรด 2 - เรียกClassicSingleton.getInstance()พร้อมกันClassicSingletonสามารถสร้างอินสแตนซ์สองอินสแตนซ์ได้หากเธรด 1 ถูกจองไว้ก่อนหลังจากที่เธรดเข้าสู่ifบล็อกและจะมีการควบคุมเธรด 2 ในภายหลัง

ดังที่คุณเห็นจากการสนทนาก่อนหน้านี้แม้ว่ารูปแบบ Singleton จะเป็นหนึ่งในรูปแบบการออกแบบที่ง่ายที่สุด แต่การนำไปใช้ใน Java นั้นทำได้ง่ายมาก ส่วนที่เหลือของบทความนี้กล่าวถึงการพิจารณาเฉพาะ Java สำหรับรูปแบบ Singleton แต่ก่อนอื่นเรามาดูทางอ้อมสั้น ๆ เพื่อดูว่าคุณสามารถทดสอบคลาสซิงเกิลตันได้อย่างไร

ทดสอบ singletons

ตลอดส่วนที่เหลือของบทความนี้ฉันใช้ JUnit ร่วมกับ log4j เพื่อทดสอบคลาสเดี่ยว หากคุณไม่คุ้นเคยกับ JUnit หรือ log4j โปรดดูแหล่งข้อมูล

ตัวอย่างที่ 2 แสดงกรณีทดสอบ JUnit ที่ทดสอบซิงเกิลตันของตัวอย่างที่ 1:

ตัวอย่างที่ 2. กรณีทดสอบซิงเกิลตัน

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private ClassicSingleton sone = null, stwo = null; private static Logger logger = Logger.getRootLogger(); public SingletonTest(String name) { super(name); } public void setUp() { logger.info("getting singleton..."); sone = ClassicSingleton.getInstance(); logger.info("...got singleton: " + sone); logger.info("getting singleton..."); stwo = ClassicSingleton.getInstance(); logger.info("...got singleton: " + stwo); } public void testUnique() { logger.info("checking singletons for equality"); 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 main: getting singleton... [java] INFO main: created singleton: [email protected] [java] INFO main: ...got singleton: [email protected] [java] INFO main: getting singleton... [java] INFO main: ...got singleton: [email protected] [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)

ดังที่แสดงในรายการก่อนหน้านี้การทดสอบอย่างง่ายของตัวอย่างที่ 2 ผ่านไปด้วยสีที่บินได้ - การอ้างอิงซิงเกิลตันทั้งสองที่ได้มาClassicSingleton.getInstance()นั้นเหมือนกันอย่างแท้จริง อย่างไรก็ตามการอ้างอิงเหล่านั้นได้มาในเธรดเดียว ส่วนถัดไปจะทดสอบความเครียดคลาสซิงเกิลของเราด้วยเธรดหลายเธรด

ข้อควรพิจารณาเกี่ยวกับมัลติเธรด

ClassicSingleton.getInstance()วิธีการของตัวอย่างที่ 1 ไม่ปลอดภัยต่อเธรดเนื่องจากรหัสต่อไปนี้:

1: if(instance == null) { 2: instance = new Singleton(); 3: }

If a thread is preempted at Line 2 before the assignment is made, the instance member variable will still be null, and another thread can subsequently enter the if block. In that case, two distinct singleton instances will be created. Unfortunately, that scenario rarely occurs and is therefore difficult to produce during testing. To illustrate this thread Russian roulette, I've forced the issue by reimplementing Example 1's class. Example 4 shows the revised singleton class:

Example 4. Stack the deck

import org.apache.log4j.Logger; public class Singleton { private static Singleton singleton = null; private static Logger logger = Logger.getRootLogger(); private static boolean firstThread = true; protected Singleton() { // Exists only to defeat instantiation. } public static Singleton getInstance() { if(singleton == null) { simulateRandomActivity(); singleton = new Singleton(); } logger.info("created singleton: " + singleton); return singleton; } private static void simulateRandomActivity() { try { if(firstThread) { firstThread = false; logger.info("sleeping..."); // This nap should give the second thread enough time // to get by the first thread.Thread.currentThread().sleep(50); } } catch(InterruptedException ex) { logger.warn("Sleep interrupted"); } } }

Example 4's singleton resembles Example 1's class, except the singleton in the preceding listing stacks the deck to force a multithreading error. The first time the getInstance() method is called, the thread that invoked the method sleeps for 50 milliseconds, which gives another thread time to call getInstance() and create a new singleton instance. When the sleeping thread awakes, it also creates a new singleton instance, and we have two singleton instances. Although Example 4's class is contrived, it stimulates the real-world situation where the first thread that calls getInstance() gets preempted.

Example 5 tests Example 4's singleton:

Example 5. A test that fails

import org.apache.log4j.Logger; import junit.framework.Assert; import junit.framework.TestCase; public class SingletonTest extends TestCase { private static Logger logger = Logger.getRootLogger(); private static Singleton singleton = null; public SingletonTest(String name) { super(name); } public void setUp() { singleton = null; } public void testUnique() throws InterruptedException { // Both threads call Singleton.getInstance(). Thread threadOne = new Thread(new SingletonTestRunnable()), threadTwo = new Thread(new SingletonTestRunnable()); threadOne.start();threadTwo.start(); threadOne.join(); threadTwo.join(); } private static class SingletonTestRunnable implements Runnable { public void run() { // Get a reference to the singleton. Singleton s = Singleton.getInstance(); // Protect singleton member variable from // multithreaded access. synchronized(SingletonTest.class) { if(singleton == null) // If local reference is null... singleton = s; // ...set it to the singleton } // Local reference must be equal to the one and // only instance of Singleton; otherwise, we have two // Singleton instances. Assert.assertEquals(true, s == singleton); } } }

Example 5's test case creates two threads, starts each one, and waits for them to finish. The test case maintains a static reference to a singleton instance, and each thread calls Singleton.getInstance(). If the static member variable has not been set, the first thread sets it to the singleton obtained with the call to getInstance(), and the static member variable is compared to the local variable for equality.

นี่คือสิ่งที่จะเกิดขึ้นเมื่อรันกรณีทดสอบ: เธรดแรกเรียกgetInstance()เข้าสู่ifบล็อกและเข้าสู่โหมดสลีป ต่อจากนั้นเธรดที่สองยังเรียกgetInstance()และสร้างอินสแตนซ์ซิงเกิลตัน จากนั้นเธรดที่สองจะตั้งค่าตัวแปรสมาชิกแบบคงที่เป็นอินสแตนซ์ที่สร้างขึ้น เธรดที่สองตรวจสอบตัวแปรสมาชิกแบบคงที่และสำเนาโลคัลเพื่อความเท่าเทียมกันและการทดสอบจะผ่านไป เมื่อเธรดแรกตื่นขึ้นมันจะสร้างอินสแตนซ์ซิงเกิลตันด้วย แต่เธรดนั้นไม่ได้ตั้งค่าตัวแปรสมาชิกแบบคงที่ (เนื่องจากเธรดที่สองได้ตั้งค่าไว้แล้ว) ดังนั้นตัวแปรสแตติกและตัวแปรโลคัลจึงไม่ซิงค์และการทดสอบ เพื่อความเท่าเทียมล้มเหลว ตัวอย่างที่ 6 แสดงผลลัพธ์กรณีทดสอบของตัวอย่างที่ 5: