Как ориентироваться в обманчиво простом шаблоне Singleton

Паттерн Singleton обманчиво прост, особенно для разработчиков Java. В этой классической статье JavaWorld Дэвид Гири демонстрирует, как разработчики Java реализуют синглтоны, с примерами кода для многопоточности, загрузчиков классов и сериализации с использованием шаблона синглтона. В заключение он рассмотрит реализацию одноэлементных реестров для определения синглтонов во время выполнения.

Иногда уместно иметь только один экземпляр класса: оконные менеджеры, диспетчеры очереди печати и файловые системы являются прототипами. Обычно к этим типам объектов, известным как одиночные объекты, обращаются разрозненные объекты во всей программной системе, и поэтому для них требуется глобальная точка доступа. Конечно, если вы уверены, что вам никогда не понадобится больше одного экземпляра, можно поспорить, что вы передумаете.

Шаблон проектирования Singleton решает все эти проблемы. С помощью шаблона проектирования Singleton вы можете:

  • Убедитесь, что создан только один экземпляр класса
  • Обеспечить глобальную точку доступа к объекту
  • Разрешить несколько экземпляров в будущем, не затрагивая клиентов одноэлементного класса

Хотя шаблон проектирования Singleton, как показано ниже на рисунке ниже, является одним из простейших шаблонов проектирования, он представляет ряд подводных камней для неосторожного разработчика Java. В этой статье обсуждается шаблон проектирования Singleton и устраняются эти подводные камни.

Подробнее о шаблонах проектирования Java

Вы можете прочитать все столбцы Дэвида Гири « Шаблоны проектирования Java» или просмотреть список последних статей JavaWorld о шаблонах проектирования Java. См. Раздел « Паттерны проектирования, общая картина » для обсуждения плюсов и минусов использования паттернов «Банда четырех». Хочу больше? Получите информационный бюллетень Enterprise Java на свой почтовый ящик.

Паттерн Синглтон

В шаблонах проектирования: элементы объектно-ориентированного программного обеспечения многократного использования группа четырех описывает шаблон Singleton следующим образом:

Убедитесь, что у класса есть только один экземпляр, и предоставьте ему глобальную точку доступа.

На рисунке ниже показана диаграмма классов паттерна проектирования Singleton.

Как видите, в шаблоне проектирования Singleton не так много всего. Синглтоны поддерживают статическую ссылку на единственный экземпляр синглтона и возвращают ссылку на этот экземпляр из статического 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()первого вызова метода. Этот метод гарантирует, что экземпляры singleton создаются только при необходимости.

Во-вторых, обратите внимание, что ClassicSingletonреализует защищенный конструктор, поэтому клиенты не могут создавать ClassicSingletonэкземпляры; однако вы можете быть удивлены, обнаружив, что следующий код совершенно законен:

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

Как может класс в предыдущем фрагменте кода, который не расширяется, ClassicSingletonсоздать ClassicSingletonэкземпляр, если ClassicSingletonконструктор защищен? Ответ заключается в том, что защищенные конструкторы могут вызываться подклассами и другими классами того же пакета . Поскольку ClassicSingletonи SingletonInstantiatorнаходятся в одном пакете (пакет по умолчанию), SingletonInstantiator()методы могут создавать ClassicSingletonэкземпляры. У этой дилеммы есть два решения: вы можете сделать ClassicSingletonконструктор закрытым, чтобы ClassicSingleton()его вызывали только методы; однако это означает, что ClassicSingletonнельзя разделить на подклассы. Иногда это желательное решение; если это так, неплохо объявить свой одноэлементный классfinal, что делает это намерение явным и позволяет компилятору применять оптимизацию производительности. Другое решение - поместить ваш одноэлементный класс в явный пакет, чтобы классы в других пакетах (включая пакет по умолчанию) не могли создавать экземпляры одноэлементных экземпляров.

Третий интересный момент ClassicSingleton: возможно иметь несколько экземпляров синглтона, если классы, загруженные разными загрузчиками классов, обращаются к синглтону. Этот сценарий не так уж надуман; например, некоторые контейнеры сервлетов используют разные загрузчики классов для каждого сервлета, поэтому, если два сервлета обращаются к синглтону, каждый из них будет иметь свой собственный экземпляр.

В-четвертых, если ClassicSingletonреализует java.io.Serializableинтерфейс, экземпляры класса могут быть сериализованы и десериализованы. Однако, если вы сериализуете одноэлементный объект и впоследствии десериализуете этот объект более одного раза, у вас будет несколько одноэлементных экземпляров.

Наконец, что, возможно, наиболее важно, ClassicSingletonкласс примера 1 не является потокобезопасным. Если два потока - мы назовем их потоком 1 и потоком 2 - вызываются ClassicSingleton.getInstance()одновременно, ClassicSingletonмогут быть созданы два экземпляра, если поток 1 вытесняется сразу после того, как он входит в ifблок, а управление впоследствии передается потоку 2.

Как видно из предыдущего обсуждения, хотя шаблон Singleton является одним из простейших шаблонов проектирования, реализовать его на Java совсем не просто. В оставшейся части этой статьи рассматриваются специфические для Java соображения для шаблона Singleton, но сначала давайте сделаем небольшой обход, чтобы увидеть, как вы можете протестировать свои классы singleton.

Тестовые синглтоны

В остальной части этой статьи я использую 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()и создает экземпляр singleton. Затем второй поток устанавливает статическую переменную-член в созданный им экземпляр. Второй поток проверяет статическую переменную-член и локальную копию на равенство, и тест проходит. Когда первый поток просыпается, он также создает экземпляр singleton, но этот поток не устанавливает статическую переменную-член (потому что второй поток уже установил ее), поэтому статическая переменная и локальная переменная не синхронизированы, а тест ибо равенство не удается. В примере 6 перечислены выходные данные тестового примера примера 5: