Aldatıcı derecede basit olan Singleton modelinde nasıl gezinilir

Singleton kalıbı aldatıcı bir şekilde basittir, hatta ve özellikle Java geliştiricileri için. Bu klasik JavaWorld makalesinde David Geary, Java geliştiricilerinin singletonları nasıl uyguladıklarını, çoklu okuma, sınıf yükleyiciler ve Singleton modelini kullanarak serileştirme için kod örnekleriyle gösterir. Çalışma zamanında singletonları belirtmek için tekli kayıtların uygulanmasına bir göz atarak bitirir.

Bazen bir sınıfın tam olarak bir örneğine sahip olmak uygun olur: pencere yöneticileri, yazdırma biriktiriciler ve dosya sistemleri prototip örneklerdir. Tipik olarak, tekil olarak bilinen bu tür nesnelere bir yazılım sistemi boyunca farklı nesneler tarafından erişilir ve bu nedenle küresel bir erişim noktası gerektirir. Elbette, asla birden fazla örneğe ihtiyacınız olmayacağından emin olduğunuzda, fikrinizi değiştireceğiniz iyi bir bahis.

Singleton tasarım modeli tüm bu endişeleri giderir. Singleton tasarım deseni ile şunları yapabilirsiniz:

  • Bir sınıfın yalnızca bir örneğinin oluşturulduğundan emin olun
  • Nesneye genel bir erişim noktası sağlayın
  • Bir singleton sınıfının istemcilerini etkilemeden gelecekte birden çok örneğe izin verin

Aşağıdaki şekilde gösterildiği gibi Singleton tasarım modeli en basit tasarım modellerinden biri olsa da, dikkatsiz Java geliştiricisi için bir takım tuzaklar sunmaktadır. Bu makale Singleton tasarım modelini tartışıyor ve bu tuzakları ele alıyor.

Java tasarım modelleri hakkında daha fazla bilgi

David Geary'nin tüm Java Tasarım Modelleri sütunlarını okuyabilir veya JavaWorld'ün Java tasarım kalıpları hakkındaki en son makalelerinin bir listesini görüntüleyebilirsiniz . Dörtlü Çete modellerini kullanmanın artıları ve eksileri hakkında bir tartışma için " Tasarım modelleri, büyük resim " bölümüne bakın . Daha fazla istemek? Enterprise Java haber bültenini gelen kutunuza alın.

Singleton modeli

In Design Patterns: Yeniden Kullanılabilir Nesne Odaklı Yazılım Öğeleri , Gang of Four böyle Singleton deseni açıklar:

Bir sınıfın yalnızca bir örneği olduğundan emin olun ve ona genel bir erişim noktası sağlayın.

Aşağıdaki şekil Singleton tasarım modeli sınıf diyagramını göstermektedir.

Gördüğünüz gibi, Singleton tasarım modelinde pek bir şey yok. Singletons, tek tekli örneğe statik bir başvuru sağlar ve bu örneğe statik bir instance()yöntemden bir başvuru döndürür .

Örnek 1, klasik bir Singleton tasarım modeli uygulamasını gösterir:

Örnek 1. Klasik tekli

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; } }

Örnek 1'de uygulanan tekli ifadenin anlaşılması kolaydır. ClassicSingletonSınıf durağan referans bir statik yalnız tekil örneğine referans ve döner muhafaza getInstance()yöntemi.

Sınıfla ilgili birkaç ilginç nokta var ClassicSingleton. İlk ClassicSingletonolarak , singleton oluşturmak için tembel örnekleme olarak bilinen bir teknik kullanır ; sonuç olarak, getInstance()yöntem ilk kez çağrılana kadar singleton örneği oluşturulmaz . Bu teknik, tekli örneklerin yalnızca ihtiyaç duyulduğunda oluşturulmasını sağlar.

İkinci olarak, ClassicSingletonkorumalı bir kurucu uyguladığına dikkat edin, böylece istemciler ClassicSingletonörnekleri başlatamaz ; ancak, aşağıdaki kodun tamamen yasal olduğunu keşfettiğinizde şaşırabilirsiniz:

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

Yapıcı korunuyorsa , önceki kod parçasındaki sınıf (genişlemeyen ClassicSingleton) nasıl bir ClassicSingletonörnek oluşturabilir ClassicSingleton? Cevap, korumalı oluşturucuların alt sınıflar ve aynı paketteki diğer sınıflar tarafından çağrılabilmesidir . Çünkü ClassicSingletonve SingletonInstantiatoraynı pakette (varsayılan paket) olduğundan, SingletonInstantiator()yöntemler ClassicSingletonörnekler oluşturabilir . Bu ikilemin iki çözümü vardır: ClassicSingletonYapıcıyı özel yapabilirsiniz, böylece sadece ClassicSingleton()yöntemler onu çağırır; ancak bu, ClassicSingletonalt sınıflara ayrılamayacağı anlamına gelir . Bazen bu arzu edilen bir çözümdür; eğer öyleyse, singleton sınıfınızı ilan etmek iyi bir fikirdirfinalBu, bu amacı açık hale getirir ve derleyicinin performans optimizasyonlarını uygulamasına izin verir. Diğer çözüm, singleton sınıfınızı açık bir pakete koymaktır, böylece diğer paketlerdeki sınıflar (varsayılan paket dahil) tekli örnekleri başlatamaz.

Üçüncü ilginç nokta ClassicSingletonşudur: Farklı sınıf yükleyiciler tarafından yüklenen sınıflar bir singleton'a erişirse birden çok tekli örneğe sahip olmak mümkündür. Bu senaryo o kadar da zorlayıcı değil; örneğin, bazı sunucu uygulaması kapsayıcıları her sunucu uygulaması için farklı sınıf yükleyiciler kullanır; bu nedenle, iki sunucu uygulaması bir teke erişirse, her birinin kendi örneği olur.

Dördüncü olarak, arabirimi ClassicSingletonuygularsa, java.io.Serializablesınıfın örnekleri serileştirilebilir ve serileştirmesi kaldırılabilir. Ancak, bir tekil nesneyi serileştirirseniz ve ardından bu nesneyi birden fazla kez serisini kaldırırsanız, birden çok tekli örneğiniz olur.

Son olarak ve belki de en önemlisi, Örnek 1'in ClassicSingletonsınıfı iş parçacığı açısından güvenli değildir. Eğer iki iş parçacığı - biz onlara İş Parçacığı 1 ve İş Parçacığı 2 adını vereceğiz - ClassicSingleton.getInstance()aynı anda ClassicSingletonçağrılırsa, Evre 1 ifbloğa girdikten hemen sonra önceliklendirilirse ve ardından iş parçacığı 2'ye kontrol verilirse iki örnek yaratılabilir .

Önceki tartışmadan da görebileceğiniz gibi, Singleton modeli en basit tasarım modellerinden biri olmasına rağmen, onu Java'da uygulamak çok basittir. Bu makalenin geri kalanı, Singleton kalıbı için Java'ya özgü hususları ele almaktadır, ancak önce tekli sınıflarınızı nasıl test edebileceğinizi görmek için kısa bir yoldan gidelim.

Tekilleri test et

Bu makalenin geri kalanında, tekli sınıfları test etmek için JUnit'i log4j ile birlikte kullanıyorum. JUnit veya log4j hakkında bilgi sahibi değilseniz, bkz. Kaynaklar.

Örnek 2, Örnek 1'in tekil durumunu test eden bir JUnit test senaryosunu listeler:

Örnek 2. Bir tekil test senaryosu

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); } }

Örnek 2'nin test durumu ClassicSingleton.getInstance()iki kez çağırır ve döndürülen referansları üye değişkenlerinde depolar. testUnique()Yöntem kontrol referanslar aynı olduğunu görmek için. Örnek 3, test senaryosu çıktısını göstermektedir:

Örnek 3. Test senaryosu çıktısı

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)

Önceki listenin gösterdiği gibi, Örnek 2'nin basit testi uçan renklerle geçmektedir - ile elde edilen iki tekli referans ClassicSingleton.getInstance()gerçekten aynıdır; ancak, bu referanslar tek bir iş parçacığında elde edilmiştir. Bir sonraki bölüm, tekli sınıfımızı birden çok iş parçacığı ile stres testi yapar.

Çoklu kullanım ile ilgili hususlar

Örnek 1'in ClassicSingleton.getInstance()yöntemi, aşağıdaki kod nedeniyle iş parçacığı açısından güvenli değildir:

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.

Test senaryosu çalıştığında şunlar olur: İlk iş parçacığı çağırır getInstance(), ifbloğa girer ve uyur. Daha sonra, ikinci iş parçacığı da getInstance()bir tekil örnek çağırır ve oluşturur. İkinci iş parçacığı daha sonra statik üye değişkenini oluşturduğu örneğe ayarlar. İkinci iş parçacığı, statik üye değişkenini ve yerel kopyayı eşitlik açısından kontrol eder ve test başarılı olur. İlk iş parçacığı uyandığında, aynı zamanda bir tekil örnek oluşturur, ancak bu iş parçacığı statik üye değişkenini ayarlamaz (çünkü ikinci iş parçacığı zaten onu ayarlamıştır), bu nedenle statik değişken ve yerel değişken eşzamanlı değildir ve test eşitlik başarısız olur. Örnek 6, Örnek 5'in test senaryosu çıktısını listeler: