믿을 수 없을 정도로 단순한 싱글 톤 패턴을 탐색하는 방법

Singleton 패턴은 특히 Java 개발자에게도 믿을 수 없을 정도로 간단합니다. 이 고전적인 JavaWorld 기사에서 David Geary는 Java 개발자가 Singleton 패턴을 사용한 멀티 스레딩, 클래스 로더 및 직렬화에 대한 코드 예제와 함께 싱글 톤을 구현하는 방법을 보여줍니다. 그는 런타임에 싱글 톤을 지정하기 위해 싱글 톤 레지스트리 구현에 대해 살펴보면서 결론을 내립니다.

때로는 정확히 하나의 클래스 인스턴스를 갖는 것이 적절합니다. 창 관리자, 인쇄 스풀러 및 파일 시스템이 전형적인 예입니다. 일반적으로 이러한 유형의 개체 (싱글 톤이라고 함)는 소프트웨어 시스템 전체에서 서로 다른 개체에 의해 액세스되므로 전역 액세스 지점이 필요합니다. 물론 하나 이상의 인스턴스가 필요하지 않다고 확신 할 때 마음이 바뀌는 것이 좋습니다.

Singleton 디자인 패턴은 이러한 모든 문제를 해결합니다. Singleton 디자인 패턴을 사용하면 다음을 수행 할 수 있습니다.

  • 클래스의 인스턴스가 하나만 생성되었는지 확인
  • 객체에 대한 글로벌 액세스 지점 제공
  • 싱글 톤 클래스의 클라이언트에 영향을주지 않고 향후 여러 인스턴스 허용

아래 그림에서 알 수 있듯이 Singleton 디자인 패턴은 가장 단순한 디자인 패턴 중 하나이지만, 조심하지 않는 Java 개발자에게는 여러 가지 함정이 있습니다. 이 기사에서는 Singleton 디자인 패턴에 대해 설명하고 이러한 함정을 해결합니다.

Java 디자인 패턴에 대한 추가 정보

David Geary의 Java Design Patterns 컬럼을 모두 읽 거나 Java 디자인 패턴에 대한 JavaWorld의 최신 기사 목록을 볼 수 있습니다. Gang of Four 패턴 사용의 장단점에 대한 논의는 " 디자인 패턴, 큰 그림 "을 참조하십시오 . 더 원해? 받은 편지함으로 전달되는 Enterprise Java 뉴스 레터를 받으십시오.

싱글 톤 패턴

에서 디자인 패턴 : 재사용 가능한 객체 지향 소프트웨어의 요소 , 네의 갱은이 같은 싱글 톤 패턴을 설명합니다 :

클래스에 인스턴스가 하나만 있는지 확인하고 이에 대한 전역 액세스 지점을 제공합니다.

아래 그림은 Singleton 디자인 패턴 클래스 다이어그램을 보여줍니다.

보시다시피 Singleton 디자인 패턴에는 많은 것이 없습니다. 싱글 톤은 유일한 싱글 톤 인스턴스에 대한 정적 참조를 유지하고 정적 instance()메서드 에서 해당 인스턴스에 대한 참조를 반환합니다 .

예제 1은 클래식 싱글 톤 디자인 패턴 구현을 보여줍니다.

예 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-create ClassicSingleton경우 생성 인스턴스를 ClassicSingleton생성자가 보호? 대답은 보호 된 생성자가 동일한 패키지의 하위 클래스와 다른 클래스에 의해 호출 될 수 있다는 것 입니다. 때문에 ClassicSingletonSingletonInstantiator같은 패키지 (기본 패키지)에, SingletonInstantiator()방법을 만들 수 있습니다 ClassicSingleton인스턴스를. 이 딜레마에는 두 가지 해결책이 있습니다. ClassicSingleton생성자를 비공개로 만들어 ClassicSingleton()메서드 만 호출 하도록 할 수 있습니다 . 그러나 이는 ClassicSingleton하위 클래스가 될 수 없음 을 의미 합니다. 때로는 이것이 바람직한 해결책입니다. 그렇다면 싱글 톤 클래스를 선언하는 것이 좋습니다.final이는 의도를 명시 적으로 만들고 컴파일러가 성능 최적화를 적용 할 수 있도록합니다. 다른 해결책은 싱글 톤 클래스를 명시 적 패키지에 넣어 다른 패키지 (기본 패키지 포함)의 클래스가 싱글 톤 인스턴스를 인스턴스화 할 수 없도록하는 것입니다.

세 번째 흥미로운 점 ClassicSingleton: 다른 클래스 로더에 의해로드 된 클래스가 싱글 톤에 액세스하는 경우 여러 싱글 톤 인스턴스를 가질 수 있습니다. 그 시나리오는 그리 어렵지 않습니다. 예를 들어, 일부 서블릿 컨테이너는 각 서블릿에 대해 별개의 클래스 로더를 사용하므로 두 서블릿이 싱글 톤에 액세스하면 각각 고유 한 인스턴스를 갖게됩니다.

넷째, 인터페이스를 ClassicSingleton구현하는 경우 java.io.Serializable클래스의 인스턴스를 직렬화 및 역 직렬화 할 수 있습니다. 그러나 싱글 톤 객체를 직렬화 한 다음 해당 객체를 두 번 이상 역 직렬화하면 여러 싱글 톤 인스턴스가 생깁니다.

마지막으로, 아마도 가장 중요한 것은 Example 1의 ClassicSingleton클래스가 스레드로부터 안전하지 않다는 것입니다. 두 개의 스레드 (Thread 1 및 Thread 2라고 함 ClassicSingleton.getInstance())가 동시에 호출 되는 ClassicSingleton경우 스레드 1이 if블록에 들어간 직후에 선점 되고 제어가 나중에 스레드 2에 주어지면 두 개의 인스턴스가 생성 될 수 있습니다 .

이전 논의에서 알 수 있듯이 Singleton 패턴은 가장 단순한 디자인 패턴 중 하나이지만 Java에서 구현하는 것은 간단하지 않습니다. 이 기사의 나머지 부분에서는 싱글 톤 패턴에 대한 Java 관련 고려 사항을 다루지 만 먼저 싱글 톤 클래스를 테스트 할 수있는 방법을 알아보기 위해 잠시 우회 해 보겠습니다.

싱글 톤 테스트

이 기사의 나머지 부분에서는 JUnit을 log4j와 함께 사용하여 싱글 톤 클래스를 테스트합니다. JUnit 또는 log4j에 익숙하지 않은 경우 참고 자료를 참조하십시오.

예제 2는 예제 1의 싱글 톤을 테스트하는 JUnit 테스트 케이스를 나열합니다.

예 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은 테스트 케이스 출력을 보여줍니다.

Example 3. Test case output

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]e86f41 [java] INFO main: checking singletons for equality [java] Time: 0.032 [java] OK (1 test)

As the preceding listing illustrates, Example 2's simple test passes with flying colors—the two singleton references obtained with ClassicSingleton.getInstance() are indeed identical; however, those references were obtained in a single thread. The next section stress-tests our singleton class with multiple threads.

Multithreading considerations

Example 1's ClassicSingleton.getInstance() method is not thread-safe because of the following code:

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의 테스트 케이스 출력을 나열합니다.