Grabduck

Шаблон проектирования Одиночка (Singleton Design Pattern)

:

Бывают такие моменты, когда нужно написать класс, который будет управлять глобальными вещами в системе. Например, это могут быть пулы потоков и соединений к БД, этот объект может управлять реестром, конфигурациями, в общем чем-то глобальным. Можно создать класс со статическими методами, но мы-то ООП-исты(!), объект нам даст значительно большую гибкость. Однако как же запретить создавать несколько объектов одного класса? Очень просто, смотрите и ликуйте в радостной агонии:

public class Singleton1 {
    private static Singleton1 instance = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance(){
        return instance;
    }
}

Как вы могли заметить, конструктор у нас приватный и никто, окромя самих “внутренностей” класса не может его вызвать. Проинициализировав объект instance, мы его возвращаем в статическом методе getInstance(). Теперь сколько бы раз мы не обращались к этому методу, всегда возвратиться один и тот же экземпляр.
Но есть здесь и недостатки:
  1. Статические поля инициализируются при первом обращении к классу, а т.к. в классе могут быть другие статические члены, константы, то обращение к ним вызовет и создание instance. С этим можно конечно смириться, но часто заказчик хочет, чтоб его приложение запускалось мгновенно, а если наш Одиночка - тяжеловес, он много чего дергает и инициализирует, а создаться он может при старте приложения, то это не есть гуд.
  2. В данном случае невозможно отлавливать исключение, если таковые возможны в конструкторе. Если же их не ожидается, то милости просим! Однако бывают случаи…
    Вторую проблему можно решить поместив создание объекта в блок static{..}.

Так же обе проблемы решаются другим способом создания Одиночки:

public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {
    }

    public static Singleton2 getInsance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

Как видите, instance не инициализируется сразу, это происходит в методе getInstance(). То есть при первом обращении к этому методу создастся и наш экземпляр. А при последующих обращениях - вернется созданный в первый раз объект. Это часто называют загрузкой по требованию(load on demand) или ленивой инициализацией(lazy initialization).
“Вот и все!”, скажете вы. Ан-нет, не все так просто. Этот метод годится в однопоточных приложениях, но чаще всего у приложения есть несколько запущенных параллельно потоков. Если два потока в данном случае одновременно дернут метод getInstance(), то они скорей всего инициализируют объект instance дважды. И привести это может к всеобщему хаосу. И мир поглотит тьма..

Третий вариант Одиночки позволяет избежать проблем с синхронизацией - просто пометить метод getInstance() модификатором synchronized:

public static synchronized Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }

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

Рассмотрим теперь один из самых популярных способов создания синхронизированного Одиночки с ленивой загрузкой:

public static Singleton4 getInstance() {
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }

Здесь две проверки на null! Называется это Double-Checked Locking. Теперь при первом обращении к методу несколько потоков могут пройти первую проверку на null, но во второй блок зайдет и инициализирует объект только один единственный поток! И синхронизация здесь может понадобиться лишь при первом обращении к методу. Ну что, мы убили всех на свете зайцев: и исключения теперь можем отлавливать, и синхронизация без потерь производительности есть, и про ленивую загрузку не забыли. Весь мир живет в ладу и мире, нет обиженных и обездоленных. Если вы в своих приложениях уже использовали такую конструкцию, то я вас поздравляю: вы допустили ошибку :) Эта ошибка может никогда не возникнуть, а может возникнуть раз в год, однако найти и отловить ее практически невозможно не зная того факта, что double-checked locking в Java - это проблема, которую не решить. И сразу крики: “Что за нелепость?! Да что здесь такого может произойти?!”. Так вот, минимум что может произойти вот что: в следствии оптимизаций JVM поля класса могут быть еще не все инициализированы, а ссылка уже может быть присвоена нашему instance, и второй поток поглядит, что instance уже не null, дорога свободна, гуляй-резвись поточная душа - возьмет объект, не ведая, что тот еще сырой. Но это не бага. При создании объекта сначала выделяется под него память, потом ссылке присваивается значение, а потом только вызывается конструктор. “На каждую хитрую задницу найдется свой винт!”, с лихвой воскликните вы и проблему эту решите каким-нибудь интересным способом, однако “на каждый винт найдется дупа с лабиринтом”, скажет вам JVM и обнаружится еще несколько фактов, о которых вы не знали. Если кого-то заинтересовало это и кто-то захочет попробовать свои силы, ознакомьтесь сначала с Java Memory Model + Double Check Logging.

Проблему эту может решить JDK 1.5 и выше с помощью ключевого слова volatile, которое синхронизирует обращение к объектам: private static volatile Singleton5 instance; А наш getInstance() останется без изменений. Однако жил-был такой серьезный дядька как Allen Holub, который заметил, что на мультипроцессорных машинах volatile приводит к серьезным проблемам с производительность. К тому же не во всех JVM volatile реализовано полноценно. Детальней читайте в статье выше и/или гуглите.

В общем, проблема с double-checked locking нерешима и в следующих версиях JVM не собирается исправляться. Во всяком случае, в обозримом будущем. Посему вернемся на землю грешную и сделаем выводы, что пока у нас есть только один вариант сделать безопасную синхронизацию и ленивую загрузку одновременно: это ключевое слово synchronized.

Приведу так же возможный пример с синхронизацией и lazy initialization, однако без возможности отлавливать исключения.

public class Singleton6 {
    private Singleton6() {
    }

    private static class Handler {
        private static Singleton6 instance = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return Handler.instance;
    }
}

Здесь у нас есть внутренний статический класс, его поле будет инициализировано при первом обращении к нему, а также обеспечена синхронизация при создании объекта тем, что это статический класс. Называется этот способ решением Билла Пью(Bill Pugh) “Initialization on Demand Holder”. Детальней - Java спецификация и google.com. При необходимости мы можем создавать объект в блоке static{...}, тогда у нас будет так же возможность перехватывать исключения:
private static class Handler {
        private static Singleton6 instance;
		static{
		    try{
	            instance = new Singleton6();
			} catch(SomeException e){
			    blah-blah
			}
		}
    }

Этого примера я, кстати, в многоуважаемом гугле не нашел(может просто плохо искал), придумал его наш Vlad, посему все почести ему :)

Последнее, самое интересное, но мало кем используемое решение:

public enum Singleton7 {
    INSTANCE;

    private Singleton7() {
    }

    public static Singleton7 getInstance(){
        return INSTANCE;
    }
}

Как видите это не что иное, как enum. По сути enum - это тот же класс, так что его можно использовать в таких целях, однако это так же может сбить с толку читателей вашего кода, ведь енум - есть енум, так что решать все же вам. С другой стороны enum можно легко сериализовать-десериализовать, в то время как обычный объект-Одиночку - не так просто привести к консенсусу, нужно сначала поплясать с бубнами, чтоб добиться такой десериализации, при которой не будет создано несколько объектов “одиночек”. Правда кому это может понадобиться я не в курсе, но, говорят, случается..

В общем из всех этих способов вы вольны выбирать любой, который подходит под вашу конкретную ситуацию. Однако полноценно все возможности предоставляет только способ Билла Пью со статическим блоком.

Теперь о подводных камнях Одиночки.

  1. Невозможность наследования. Приватный конструктор + статический метод бьют по демографии классов - размножать детей Одиночки мы не можем и для пущей уверенности можем пометить класс как final. Да и если вы захотите расширять Одиночку - задумайтесь, ибо у вас скорей всего кривой дизайн.
  2. Одиночка нарушает принцип “single class - single responsibility”, т.к. отвечает он и за свое создание, и за первоначально доверенные цели.
  3. В тестах нельзя подсунуть классу mock на Singleton. Почему? См. пункт 1. Люди, использующие мок-фреймворки для написания тестов, поймут о чем речь.
  4. Когда Одиночка не так уж и одинок…
    4.1 Если приложение использует не одну JVM, то у вас могут создаться несколько экземпляров Одиночки. Обращаю на этот факт внимание пользователей EJB, Jini, RMI.
    4.2 Если в приложении используются несколько Class Loader’ов, то каждый из них загрузит по одному экземпляру Одиночки. Так, например, некоторые серверы приложений(application server) имеют на каждый сервлет по Class Loader’у. Если у вас возникла ситуация с несколькими загрузчиками классов, то ручное управление ими может помочь, а так же может помочь отказ от Одиночки :)
    4.3 Как уже было сказано выше, если неправильно разобраться с сериализацией, то в конце концов получите несколько объектов Singleton’a.
  5. Если вы все еще пользуетесь JDK 1.2(аминь!), то лучше не шутите с Одиночкой, т.к. в Garbage Collector’е той версии есть бага - он собирает все объекты, на которые нет внешних ссылок. То есть то, что в самом классе ссылка на объект есть, его волнует в последнюю очередь, - объект он скушает все одно.

В связи со всеми вышеуказанными недостатками Одиночки, его часто относят к антипаттерну, другие же оспаривают это мнение и говорят, что им просто нужно уметь правильно пользоваться. Те же Gang of Four(корифеи ООП) до сих пор от него не отказались, хотя с момента издания их книги о шаблонах прошло много времени и многое можно было переосмыслить. Однако и противники, и приверженцы сходятся в одном: если есть возможность не применять этот шаблон, - не применяйте, чаще всего можно найти более правильное решение.

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

Singleton JT Webinar