Шаблоны проектирования: Singleton, Часть 1

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

В части 1 мы рассмотрим основные положения касающиеся шаблона, а также разберем сильные и слабые стороны нескольких основных его реализаций.

Введение

Синглтон является обычным классом для которого может быть создан только один объект в пределах жизненного цикла домена приложения.

Синглтон реализуется для тех классов, для которых наличие более одного объекта в приложении может привести к дефектам или к напрасному расходу оперативной памяти. Например, если каждый раз при необходимости залогировать информацию создавать новый объект класса Logger, то это приведет к засорению памяти приложения большим количеством одинаковых объектов. Два или более объекта класса MemoryCache приведут к проблемам производительности, ведь запрашиваемые данные, которых нет в одном объекте кеша могут находиться в другом. Синглтон используют когда необходимо предоставить глобальную точку доступа к объекту и для классов без состояния (хранение настроек конфигурационных файлов, логирование и тд).

Существует несколько различных реализаций шаблона, однако всех их объединяет наличие следующих конструкций:

  • приватный конструктор — запрещает создание объекта класса за его пределами через оператор new (обязательно!)
  • приватное статическое поле — хранит ссылку на объект класса (необязательно, можно использовать автоматическое свойство)
  • публичное статическое свойство — возвращает ссылку на объект и обычно именуется Instance (необязательно, можно использовать только поле)

Синглтоны поддерживают ленивую инициализацию, то есть объект создается при первом к нему обращении, а уничтожается только при выгрузке домена приложения, так как ссылка на объект синглтон хранится в статическом поле.

Реализация #1: Ленивая и потокоНЕбезопасная

public class Database
{
    private Database() { }

    private static Database _instance = null;
    public static Database Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new Database();
            }
            return _instance;
        }
    }
}

Преимущество данной реализации заключается в ее «ленивости» — объект будет создан только при первом обращении к свойству Instance и никак не раньше. Также в плюсы можно добавить и простоту кода, который будет понятен любому студенту. Но главным недостатком реализации, который перечеркивает все достоинства является отсутствие потокобезопасности. Два потока могут одновременно войти в блок if до инициализации свойства _instance и создать два объекта класса. По этой причине данная реализация категорически противопоказана в многопоточных приложениях.

Реализация #2: Ленивая и потокобезопасная

public class Database
{
    private Database() { }

    private static readonly object syncRoot = new object();

    private static Database _instance = null;
    public static Database Instance
    {
        get
        {
            lock (syncRoot)
            {
                if (_instance == null)
                {
                    _instance = new Database();
                }
                return _instance;
            }
        }
    }
}

Данная реализация является потокобезопасной, ведь применяемая конструкция lock создает критическую секцию в которой может находиться только один поток. Если в момент инициализации синглтона первым потоком подоспеют другие потоки, то они будут ждать завершения работы первого. Также существует более продвинутая реализация данного подхода с использованием блокировки с двойной проверкой, которая улучшает производительность приложения в случае обильного обращения кодом к свойству Instance и, следовательно, частому получению блокировки.

Реализация #3: Неленивая и потокобезопасная

public class Database
{
    private Database() { }

    public static Database Instance { get; }

    static Database()
    {
        Instance = new Database();
    }
}

Несмотря на отсутствие конструкций синхронизации потоков данный пример является потокобезопасным, так как инициализация свойства Instance выполняется в статическом конструкторе, который вызывается только один раз в пределах жизненного цикла домена приложения и потокобезопасность которого гарантируется CLR. С другой стороны данная реализация является только частично ленивой, так как свойство Instance может быть проинициализировано задолго до обращения к нему (при обращении к любому другому возможному статическому свойству или методу класса Database), что не является приемлемым в случаях, когда создание синглтона требует большого количества процессорного времени и ресурсов.

Реализация #4: Частично ленивая и потокобезопасная

public class Database
{
    private Database() { }

    public static readonly Database Instance = new Database();
}

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

public class Database
{
    private Database() { }

    static Database()
    {
        Instance = new Database();
    }

    public static readonly Database Instance = null;
}

Посмотрев на сгенерированный код, можно смело утверждать, что отличий от реализации #3 нет никаких (то что в этом примере используется поле, а в предыдущем свойство, не играет никакой роли). Но существует важное отличие. Если программист не объявил явно статический конструктор, то конструктор предоставленный компилятором отработает ленивым образом, непосредственно перед обращением к статическому свойству (при отсутствия явного статического конструктора, компилятор помечает класс атрибутом beforeFieldInit. Корректно работает начиная с версии .NET 4.0).

Посмотрите на немного расширенный пример:

public class Database
{
    private Database()
    {
        WriteLine("Instance created");
    }

    //static Database() { }

    public static readonly Database Instance = new Database();

    public static void Do()
    {
        WriteLine("Do something...");
    }
}

public class Program
{
    public static void Main()
    {
        Database.Do();
    }
}

В случае с закомментированным конструктором на экран выведется только «Do something…». Если раскомментировать конструктор, то в добавок появится и «Instance created» (валидно только для release режима).

5. Ленивая и потокобезопасная с использованием класса Lazy

public class Database
{
    private Database() { }

    private static readonly Lazy<Database> _instance
        = new Lazy<Database>(() => new Database());

    public static Database Instance => _instance.Value;
}

Класс Lazy позволяет реализовать синглтон по-настоящему ленивым и потокобезопасным. В добавок, Lazy реализует прием блокировки с двойной проверкой, что является более эффективным приемом синхронизации с точки зрения производительности. Также Lazy позволяет отключить режим потокобезопасности (true вторым параметром) и проверить создан ли уже объект свойством IsValueCreated.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s