Grabduck

skipy.ru: Записки трезвого практика -> Техника -> Реализация шаблона Singleton

:

Последнее изменение: 13 июля 2010г.

Для начала хочу сказать, что такое вообще Singleton и когда он может быть полезен. Это шаблон, который обеспечивает существование одного (реже более одного) экземпляра класса, без возможности прямого создания этого класса. Соответственно, полезен он именно в тех ситуациях, где такое поведение необходимо. Скажем, в приложении может быть только один менеджер работы с базой данных, а создание еще одного не только нецелесообразно, но и вредно. Чтобы предотвратить саму такую возможность как раз и используется Singleton.

По здравому размышлению я решил дать тут некоторое разъяснение. Singleton – это вовсе не обязательно единственный экземпляр класса. Таких экземпляров может быть несколько. Скажем, каждый элемент перечисления

public enum Digit{
    zero, one, two, three, four, five, six, seven, eight, nine
}

... существует в единственном экземпляре и является экземпляром класса Digit. Никаким образом не может быть создан второй экземпляр любого из этих объектов. Следовательно, класс перечисления Digit соответствует шаблону Singleton. Точно так же ему будет соответствовать перечисление, реализованное вручную ( private-конструктор и набор public static final полей – элементов перечисления). Ключевая особенность этого шаблона, таким образом, – отсутствие возможности создания экземпляров класса, кроме предоставляемых.

Техника реализации шаблона Singleton зависит от двух аспектов:

Ниже мы рассмотрим требования каждого из них.

Тип создаваемого объекта

С точки зрения реализации Singleton существуют два типа объектов, которые необходимо создавать: абстрактные (т.е. нам необходим единственный экземпляр интерфейса или абстрактного класса) и конкретные классы.

Необходимость создавать интерфейсы или абстрактные классы возникает довольно часто. Классический случай – в библиотеку включены интерфейсы-описания объектов и закрытая реализация. Пользователь библиотеки имеет дело только с интерфейсами. Пример такого подхода есть в Java 1.4+ – поддержка XML. Пакет javax.xml.parsers содержит абстрактный класс DocumentBuilder, который в системе нужен только в одном экземпляре.

Реализация заключается в следующем. Создается вспомогательный класс – factory (на самом деле это применение еще одного шаблона, который так и называется – Factory). Этот класс, как правило, не может быть создан напрямую – например, он тоже абстрактный, или не имеет public-конструкторов. Класс factory создает экземпляр нужного объекта (как именно – неважно, например, реализуя нужный интерфейс или наследуя свой внутренний класс от нужного абстрактного класса) и возвращает его при вызове статического метода. Пример такого подхода приведен ниже. (Я прошу не рассматривать этот код как руководство к действию, ибо в нем я намеренно не учитываю моменты, которые будут рассмотрены далее.)

public interface SomeInterface{
    public void doSomething();
}

public class SomeInterfaceFactory{

    private static SomeInterface impl = null;

    private SomeInterfaceFactory(){
    }

    public static SomeInterface getSomeInterface(){
        if (impl == null){
            impl = new SomeInterfaceImpl();
        }
        return impl;
    }

    private class SomeInterfaceImpl implements SomeInterface{
        public void doSomething(){
            // ... implementation
        }
    }
}

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

Этот подход хорошо работает в случае с интерфейсом или абстрактным классом. Более того, в этом случае он единственный. В случае же с конкретным классом у него имеется существенный, на мой взгляд, недостаток.

Дело в том, что объекту factory надо каким-то образом создавать нужный класс. Это означает, что у этого класса должен быть конструктор, доступный извне. Но! Если этот конструктор доступен кому-то еще – он доступен всем. Не спасет даже доступ по умолчанию, т.к. никто не сможет помешать создать свой класс в необходимом пакете и вызвать конструктор, создав тем самым новый объект.

Правда, если классы упакованы в jar-файл, то не все так плохо – нужный пакет можно объявить как закрытый (sealed), что означает, что все классы этого пакета должны быть в одном архиве. О том, как это сделать, можно прочитать вот тут: http://java.sun.com/docs/books/tutorial/deployment/jar/sealman.html. Соответственно, использовать конструктор с доступом по умолчанию фабрика сможет, а любой код, не находящийся в данном jar-файле – нет. Однако об этой возможности знают далеко не все.

Потому, в случае конкретного класса технику лучше изменить:

public class SingletonImpl{

    private static SingletonImpl self = new SingletonImpl();

    private SingletonImpl(){
        super();
        // perform initialization here
    }

    public static  SingletonImpl getInstance(){
        return self;
    }
}

Как видно из приведенного кода, нужный объект создается в момент загрузки класса и дальше выдается при всех обращениях к SingletonImpl.getInstance(). При этом используется private-конструктор, что делает невозможным создание этого объекта извне.

С различиями, обусловленными разными типами классов, закончили. Переходим к следующей части.

Инициализация и синхронизация

Вернемся к примеру, рассмотренному только что. Его недостаток очевиден: объект создается всегда, вне зависимости от того, будем мы использовать его или нет. Если создание объекта требует много ресурсов – такой вариант не самый удачный.

В этом случае может спасти так называемая отложенная инициализация – создание объекта в тот момент, когда он требуется. Пример приведен ниже.

public class SingletonImpl{

    private static SingletonImpl self = null;

    private SingletonImpl(){
        super();
        // perform initialization here
        self = this;
    }

    public static SingletonImpl getInstance(){
        return (self == null) ? new SingletonImpl() : self;
    }
}

Как видно из кода, нужный объект создается при первом обращении к SingletonImpl.getInstance(). Конструктор private, так что создание объекта извне невозможно. Статическая ссылка на единственный экземпляр объекта инициализируется при вызове конструктора, так что при последующих вызовах именно этот объект и будет выдан.

Хорошо? На самом деле, не очень. Дело вот в чем. При практически одновременном первом обращении к SingletonImpl.getInstance() из двух разных потоков может возникнуть такая ситуация: каждый из потоков увидит переменную self, имеющую значение null. В результате каждый из них начнет создание нового экземпляра. Хорошо, если это безболезненная процедура – тогда переменная self будет просто ссылаться на второй из созданных объектов, а первый будет в дальнейшем убран сборщиком мусора. Да и в этом случае возможны осложнения – если приложение где-нибудь сохранит ссылку на получанный объект, в рассчете на то, что она неизменна, в системе будет ДВА экземпляра вместо одного. А если инициализация блокирует какие-либо ресурсы, так что второй объект просто не будет создан? Получится не совсем то, чего мы добивались.

Для того, чтобы избежать такой ситуации, нужно синхронизировать доступ к переменной self. Наилучшим способом является следующий: объявить метод getInstance как synchronized. В этом случае виртуальная машина гарантирует, что в каждый момент времени только один поток имеет возможность исполнять код метода getInstance, и подобных коллизий возникнуть не должно. Таким образом, код превращается в следующий:

public class SingletonImpl{

    private static SingletonImpl self = null;

    private SingletonImpl(){
        super();
        // perform initialization here
        self = this;
    }

    public static synchronized SingletonImpl getInstance(){
        return (self == null) ? new SingletonImpl() : self;
    }
}

Я бы сказал, что это – окончательный вариант реализации Singleton в случае отложенной инициализации.

Однако этот вариант вызывает критику у некоторых разработчиков. Их аргументы – синхронизация нужна только один раз, при создании экземпляра, а накладные расходы она дает при каждом вызове.

Теоретически это верно. Правда, тут есть одно "но". Первое – современные виртуальные машины имеют крайне низкие накладные расходы, связанные с синхронизацией. А заниматься оптимизацией именно этого фрагмента, не будучи уверенным, что именно он является узким местом в производительности... Как говорил Дональд Кнут, "Преждевременная оптимизация является первопричиной всех бед в программировании".

Эти соображения, тем не менее, не находят понимания. В результате я видел следующий вариант метода getInstance():

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

Этот прием носит специальное название – Double-checked locking. Теоретически он вполне понятен, более того, когда-то он считался одним из правильных шаблонов. Проверка, потом синхронизация, потом еще одна проверка – на случай, если за это время другой поток уже успел создать объект. Практически же такой подход ничего не гарантирует. В отсутствие синхронизации на уровне метода поток может не увидеть изменения, сделаные другим потоком. Почему именно – я не буду сейчас описывать. Это означало бы просто переписать страницу из книги Джошуа Блоха. Потому всех интересующихся отсылаю к этой книге: Джошуа Блох. Java. Эффективное программирование. Нужная статья – 48. Блох рассматривает как раз этот вариант реализации отложенной инициализации.

Также все желающие могут ознакомиться вот с этой статьей с сайта IBM DeveloperWorks – Double-checked locking and the Singleton pattern (http://www.ibm.com/developerworks/java/library/j-dcl.html). В ней рассматривается вариант отложенной инициализации с двойной проверкой, причем вплоть до уровня ассемблера.

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

Собственно, это всё о реализации Singleton. Надеюсь, эта статья окажется полезной. Всем спасибо за внимание!