Паттерн Одиночка (Singleton) - описание (шаблон). Использование паттерна синглтон Потокобезопасная реализация без использования lock

22.10.2015
23:12

Назначение паттерна Синглтон (или Одиночка) заключается в обеспечении доступа к некоторому уникальному объекту из любой точки приложения. Под уникальностью подразумевается, что такой объект существует в памяти в единственном экземпляре и другие экземпляры созданы быть не могут.

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

Пример реализация паттерна Singleton на C++

Технически реализовать объект-Синглтон и использовать его в приложении довольно просто (что подкупает):

#include class MySingleton { public: // Функция-член для доступа к единственному экземпляру static MySingleton* getInstance() { static MySingleton instance; return &instance; } // Наполняем полезным функционалом, как и любой другой класс void test() { std::cout << "Singleton test" << std::endl; } private: // Объявляем конструктор закрытым, чтобы нельзя было // создавать экземпляры класса извне MySingleton() { } }; int main() { // Используем Синглтон MySingleton::getInstance()->test(); // А это работать не будет, поскольку конструктор - закрытый // MySingleton singleton; return 0; }

При желании объект-Синглтон можно адаптировать к многопоточной среде выполнения с помощью мьютексов.

Реклама

Всегда думайте перед созданием Синглтона

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

Рассмотрим пример. Может показаться хорошей идеей создать класс для управления настройками приложения в виде Синглтона. Тогда все компоненты приложения смогут видеть необходимые опции и легко их использовать. С одной стороны, идея кажется довольно неплохой. Конфигурация приложения действительно может быть представлена уникальной сущностью. А свободный доступ к Синглтону упростит использование конфигурации.

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

К тому же, паттерн Синглтон усложняет использование полиморфизма и других прелестей ООП, которые могут понадобится, когда уже минимальным рефакторингом обойтись не удастся.

Хотя есть и вполне безобидные применения Синглтонов. Например, при реализации другого паттерна: Абстрактная Фабрика.

Также использование Синглтона оправдано для представления физически уникальных ресурсов компьютера. Например, систему слежения за подключением/отключением USB-устройств уместно реализовать в виде Синглтона.

Назначение паттерна Singleton

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

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

Описание паттерна Singleton

Архитектура паттерна Singleton основана на идее использования глобальной переменной, имеющей следующие важные свойства:

  1. Такая переменная доступна всегда. Время жизни глобальной переменной - от запуска программы до ее завершения.
  2. Предоставляет глобальный доступ, то есть, такая переменная может быть доступна из любой части программы.

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

Для решения этой проблемы паттерн Singleton возлагает контроль над созданием единственного объекта на сам класс. Доступ к этому объекту осуществляется через статическую функцию-член класса, которая возвращает указатель или ссылку на него. Этот объект будет создан только при первом обращении к методу, а все последующие вызовы просто возвращают его адрес. Для обеспечения уникальности объекта, конструкторы и оператор присваивания объявляются закрытыми.

Паттерн Singleton часто называют усовершенствованной глобальной переменной.

Реализация паттерна Singleton

Классическая реализация Singleton

Рассмотрим наиболее часто встречающуюся реализацию паттерна Singleton.

// Singleton.h class Singleton { private: static Singleton * p_instance; // Конструкторы и оператор присваивания недоступны клиентам Singleton() {} Singleton(const Singleton&); Singleton& operator=(Singleton&); public: static Singleton * getInstance() { if(!p_instance) p_instance = new Singleton(); return p_instance; } }; // Singleton.cpp #include "Singleton.h" Singleton* Singleton::p_instance = 0;

Клиенты запрашивают единственный объект класса через статическую функцию-член getInstance() , которая при первом запросе динамически выделяет память под этот объект и затем возвращает указатель на этот участок памяти. Впоследcтвии клиенты должны сами позаботиться об освобождении памяти при помощи оператора delete .

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

Singleton Мэйерса

// Singleton.h class Singleton { private: Singleton() {} Singleton(const Singleton&); Singleton& operator=(Singleton&); public: static Singleton& getInstance() { static Singleton instance; return instance; } };

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

Приведенная реализация паттерна Singleton использует так называемую отложенную инициализацию (lazy initialization) объекта, когда объект класса инициализируется не при старте программы, а при первом вызове getInstance() . В данном случае это обеспечивается тем, что статическая переменная instance объявлена внутри функции - члена класса getInstance() , а не как статический член данных этого класса. Отложенную инициализацию, в первую очередь, имеет смысл использовать в тех случаях, когда инициализация объекта представляет собой дорогостоящую операцию и не всегда используется.

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

Улучшенная версия классической реализации Singleton

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

// Singleton.h class Singleton; // опережающее объявление class SingletonDestroyer { private: Singleton* p_instance; public: ~SingletonDestroyer(); void initialize(Singleton* p); }; class Singleton { private: static Singleton* p_instance; static SingletonDestroyer destroyer; protected: Singleton() { } Singleton(const Singleton&); Singleton& operator=(Singleton&); ~Singleton() { } friend class SingletonDestroyer; public: static Singleton& getInstance(); }; // Singleton.cpp #include "Singleton.h" Singleton * Singleton::p_instance = 0; SingletonDestroyer Singleton::destroyer; SingletonDestroyer::~SingletonDestroyer() { delete p_instance; } void SingletonDestroyer::initialize(Singleton* p) { p_instance = p; } Singleton& Singleton::getInstance() { if(!p_instance) { p_instance = new Singleton(); destroyer.initialize(p_instance); } return *p_instance; }

Ключевой особенностью этой реализации является наличие класса SingletonDestroyer , предназначенного для автоматического разрушения объекта Singleton. Класс Singleton имеет статический член SingletonDestroyer , который инициализируется при первом вызове Singleton::getInstance() создаваемым объектом Singleton . При завершении программы этот объект будет автоматически разрушен деструктором SingletonDestroyer (для этого SingletonDestroyer объявлен другом класса Singleton).

Для предотвращения случайного удаления пользователями объекта класса Singleton , деструктор теперь уже не является общедоступным как ранее. Он объявлен защищенным.

Использование нескольких взаимозависимых одиночек

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

  • Как гарантировать, что к моменту использования одного одиночки, экземпляр другого зависимого уже создан?
  • Как обеспечить возможность безопасного использования одного одиночки другим при завершении программы? Другими словами, как гарантировать, что в момент разрушения первого одиночки в его деструкторе еще возможно использование второго зависимого одиночки (то есть второй одиночка к этому моменту еще не разрушен)?

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

// Singleton.h class Singleton1 { private: Singleton1() { } Singleton1(const Singleton1&); Singleton1& operator=(Singleton1&); public: static Singleton1& getInstance() { static Singleton1 instance; return instance; } }; class Singleton2 { private: Singleton2(Singleton1& instance): s1(instance) { } Singleton2(const Singleton2&); Singleton2& operator=(Singleton2&); Singleton1& s1; public: static Singleton2& getInstance() { static Singleton2 instance(Singleton1::getInstance()); return instance; } }; // main.cpp #include "Singleton.h" int main() { Singleton2& s = Singleton2::getInstance(); return 0; }

Объект Singleton1 гарантированно инициализируется раньше объекта Singleton2 , так как в момент создания объекта Singleton2 происходит вызов Singleton1::getInstance() .

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

Несмотря на кажущуюся простоту паттерна Singleton (используется всего один класс), его реализация не является тривиальной.

Результаты применения паттерна Singleton

Достоинства паттерна Singleton

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

Недостатки паттерна Singleton

  • В случае использования нескольких взаимозависимых одиночек их реализация может резко усложниться.

Последнее обновление: 23.12.2018

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

Когда надо использовать Синглтон? Когда необходимо, чтобы для класса существовал только один экземпляр

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

Классическая реализация данного шаблона проектирования на C# выглядит следующим образом:

Class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) instance = new Singleton(); return instance; } }

В классе определяется статическая переменная - ссылка на конкретный экземпляр данного объекта и приватный конструктор. В статическом методе getInstance() этот конструктор вызывается для создания объекта, если, конечно, объект отсутствует и равен null.

Для применения паттерна Одиночка создадим небольшую программу. Например, на каждом компьютере можно одномоментно запустить только одну операционную систему. В этом плане операционная система будет реализоваться через паттерн синглтон:

Class Program { static void Main(string args) { Computer comp = new Computer(); comp.Launch("Windows 8.1"); Console.WriteLine(comp.OS.Name); // у нас не получится изменить ОС, так как объект уже создан comp.OS = OS.getInstance("Windows 10"); Console.WriteLine(comp.OS.Name); Console.ReadLine(); } } class Computer { public OS OS { get; set; } public void Launch(string osName) { OS = OS.getInstance(osName); } } class OS { private static OS instance; public string Name { get; private set; } protected OS(string name) { this.Name=name; } public static OS getInstance(string name) { if (instance == null) instance = new OS(name); return instance; } }

Синглтон и многопоточность

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

Static void Main(string args) { (new Thread(() => { Computer comp2 = new Computer(); comp2.OS = OS.getInstance("Windows 10"); Console.WriteLine(comp2.OS.Name); })).Start(); Computer comp = new Computer(); comp.Launch("Windows 8.1"); Console.WriteLine(comp.OS.Name); Console.ReadLine(); }

Здесь запускается дополнительный поток, который получает доступ к синглтону. Параллельно выполняется тот код, который идет запуска потока и кторый также обращается к синглтону. Таким образом, и главный, и дополнительный поток пытаются инициализровать синглтон нужным значением - "Windows 10", либо "Windows 8.1". Какое значение сиглтон получит в итоге, пресказать в данном случае невозможно.

Вывод программы может быть такой:

Windows 8.1 Windows 10

Или такой:

Windows 8.1 Windows 8.1

В итоге мы сталкиваемся с проблемой инициализации синглтона, когда оба потока одновременно обращаются к коду:

If (instance == null) instance = new OS(name);

Чтобы решить эту проблему, перепишем класс синглтона следующим образом:

Class OS { private static OS instance; public string Name { get; private set; } private static object syncRoot = new Object(); protected OS(string name) { this.Name = name; } public static OS getInstance(string name) { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new OS(name); } } return instance; } }

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

Другие реализации синглтона

Выше были рассмотрены общие стандартные реализации: потоконебезопасная и потокобезопасная реализации паттерна. Но есть еще ряд дополнительных реализаций, которые можно рассмотреть.

Потокобезопасная реализация без использования lock

public class Singleton { private static readonly Singleton instance = new Singleton(); public string Date { get; private set; } private Singleton() { Date = System.DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { return instance; } }

Данная реализация также потокобезопасная, то есть мы можем использовать ее в потоках так:

(new Thread(() => { Singleton singleton1 = Singleton.GetInstance(); Console.WriteLine(singleton1.Date); })).Start(); Singleton singleton2 = Singleton.GetInstance(); Console.WriteLine(singleton2.Date);

Lazy-реализация

Определение объекта синглтона в виде статического поля класса открывает нам дорогу к созданию Lazy-реализации паттерна Синглтон, то есть такой реализации, где данные будут инициализироваться только перед непосредственным использованием. Поскольку статические поля инициализируются перед первым доступом к статическому членам класса и перед вызовом статического конструктора (при его наличии). Однако здесь мы можем столкнуться с двумя трудностями.

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

Public class Singleton { private static readonly Singleton instance = new Singleton(); public static string text = "hello"; public string Date { get; private set; } private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = System.DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); Thread.Sleep(500); return instance; } } class Program { static void Main(string args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Console.Read(); } }

В данном случае идет только обращение к переменной text, однако статическое поле instance также будет инициализировано. Например, консольный вывод в данном случае мог бы выглядеть следующим образом:

Singleton ctor 16:05:54.1469982 Main 16:05:54.2920316 hello

В данном случае мы видим, что статическое поле instance инициализировано.

Для решения этой проблемы выделим отдельный внутренний класс в рамках класса синглтона:

Public class Singleton { public string Date { get; private set; } public static string text = "hello"; private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); return Nested.instance; } private class Nested { internal static readonly Singleton instance = new Singleton(); } } class Program { static void Main(string args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Console.Read(); } }

Теперь статическая переменная, которая представляет объект синглтона, определена во вложенном классе Nested. Чтобы к этой переменной можно было обращаться из класса синглтона, она имеет модификатор internal, в то же время сам класс Nested имеет модификатор private, что позволяет гарантировать, что данный класс будет доступен только из класса Singleton.

Консольный вывод в данном случае мог бы выглядеть следующим образом:

Main 16:11:40.1320873 hello

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

Например, рассмотрим выполнение следующей программы:

Static void Main(string args) { Console.WriteLine($"Main {DateTime.Now.TimeOfDay}"); Console.WriteLine(Singleton.text); Singleton singleton1 = Singleton.GetInstance(); Console.WriteLine(singleton1.Date); Console.Read(); }

Ее возможный консольный вывод:

Main 16:33:33.1404818 hello Singleton ctor 16:33:33.1564802 GetInstance 16:33:33.1574824 16:33:33.1564802

Мы видим, что код метода GetInstance, который идет до вызова конструктора класса Singleton, выполняется после выполнения этого конструктора. Поэтому добавим в выше определенный класс Nested статический конструктор:

Public class Singleton { public string Date { get; private set; } public static string text = "hello"; private Singleton() { Console.WriteLine($"Singleton ctor {DateTime.Now.TimeOfDay}"); Date = DateTime.Now.TimeOfDay.ToString(); } public static Singleton GetInstance() { Console.WriteLine($"GetInstance {DateTime.Now.TimeOfDay}"); Thread.Sleep(500); return Nested.instance; } private class Nested { static Nested() { } internal static readonly Singleton instance = new Singleton(); } }

Теперь при выполнении той же программы мы получим полноценную Lazy-реализацию:

Main 16:37:18.4108064 hello GetInstance 16:37:18.4208062 Singleton ctor 16:37:18.4218065 16:37:18.4228061

Реализация через класс Lazy

Еще один способ создания синглтона представляет использование класса Lazy:

Public class Singleton { private static readonly Lazy lazy = new Lazy(() => new Singleton()); public string Name { get; private set; } private Singleton() { Name = System.Guid.NewGuid().ToString(); } public static Singleton GetInstance() { return lazy.Value; } }

Подводные камни Singleton: почему самый известный шаблон проектирования нужно использовать с осторожностью

Паттерн - пожалуй, самый известный паттерн проектирования. Тем не менее, он не лишен недостатков, поэтому некоторые программисты (например, Егор Бугаенко) считают его антипаттерном. Разбираемся в том, какие же подводные камни таятся в Singleton’е.

Определение паттерна

Само описание паттерна достаточно простое - класс должен гарантированно иметь лишь один объект, и к этому объекту должен быть предоставлен глобальный доступ. Скорее всего, причина его популярности как раз и кроется в этой простоте - всего лишь один класс, ничего сложного. Это, наверное, самый простой для изучения и реализации паттерн. Если вы встретите человека, который только что узнал о существовании паттернов проектирования, можете быть уверены, что он уже знает про Singleton. Проблема заключается в том, что когда из инструментов у вас есть только молоток, всё вокруг выглядит как гвозди. Из-за этого “Одиночкой” часто злоупотребляют.

Простейшая реализация

Как уже говорилось выше, в этом нет ничего сложного:

  • Сделайте конструктор класса приватным, чтобы не было возможности создать экземпляр класса извне.
  • Храните экземпляр класса в private static поле.
  • Предоставьте метод, который будет давать доступ к этому объекту.
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } }

Принцип единственной обязанности

В объектно-ориентированном программировании существует правило хорошего тона - “Принцип едиственной обязанности” (Single Responsibility Principle, первая буква в аббревиатуре SOLID). Согласно этому правилу, каждый класс должен отвечать лишь за один какой-то аспект. Совершенно очевидно, что любой Singleton-класс отвечает сразу за две вещи: за то, что класс имеет лишь один объект, и за реализацию того, для чего этот класс вообще был создан.

Принцип единственной обязанности был создан не просто так - если класс отвечает за несколько действий, то, внося изменения в один аспект поведения класса, можно затронуть и другой, что может сильно усложнить разработку. Так же разработку усложняет тот факт, что переиспользование (reusability) класса практически невозможно. Поэтому хорошим шагом было бы, во-первых, вынести отслеживание того, является ли экземпляр класса единственным, из класса куда-либо во вне, а во-вторых, сделать так, чтобы у класса, в зависимости от контекста, появилась возможность перестать быть Singleton’ом, что позволило бы использовать его в разных ситуациях, в зависимости от необходимости (т.е. с одним экземпляром, с неограниченным количество экземпляров, с ограниченным набором экземпляров и так далее).

Тестирование

Один из главных минусов паттерна “Одиночка” - он сильно затрудняет юнит-тестирование. “Одиночка” привносит в программу глобальное состояние, поэтому вы не можете просто взять и изолировать классы, которые полагаются на Singelton. Поэтому, если вы хотите протестировать какой-то класс, то вы обязаны вместе с ним тестировать и Singleton, но это ещё полбеды. Состояние “Одиночки” может меняться, что порождает следующие проблемы:

  • Порядок тестов теперь имеет значение;
  • Тесты могут иметь нежелательные сторонние эффекты, порождённые Singleton’ом;
  • Вы не можете запускать несколько тестов параллельно;
  • Несколько вызовов одного и того же теста могут приводить к разным результатам.

На эту тему есть отличный доклад с “Google Tech Talks”:

Скрытые зависимости

Обычно, если классу нужно что-то для работы, это сразу понятно из его методов и конструкторов. Когда очевидно, какие зависимости есть у класса, гораздо проще их предоставить. Более того, в таком случае вы можете использовать вместо реально необходимых зависимостей заглушки для тестирования. Если же класс использует Singleton, это может быть совершенно не очевидно. Всё становится гораздо хуже, если экземпляру класса для работы необходима определённая инициализация (например, вызов метода init(...) или вроде того). Ещё хуже, если у вас существует несколько Singleton’ов, которые должны быть созданы и инициализированы в определённом порядке.

Загрузчик класса

Если говорить о Java, то обеспечение существования лишь одного экземпляра класса, которое так необходимо для Singleton, становится всё сложнее. Проблема в том, что классическая реализация не проверяет, существует ли один экземпляр на JVM, он лишь удостоверяется, что существует один экземпляр на classloader. Если вы пишете небольшое клиентское приложение, в котором используется лишь один classloader, то никаких проблем не возникнет. Однако если вы используете несколько загрузчиков класса или ваше приложение должно работать на сервере (где может быть запущено несколько экземпляров приложения в разных загрузчиках классов), то всё становится очень печально.

Десериализация

Ещё один интересный момент заключается в том, что на самом деле стандартная реализация Singleton не запрещает создавать новые объекты. Она запрещает создавать новые объекты через конструктор . А ведь существуют и другие способы создать экземпляр класса, и один из них - сериализация и десериализация. Полной защиты от намеренного создания второго экземпляра Singelton’а можно добиться только с помощью использования enum’а с единственным состоянием, но это - неоправданное злоупотребление возможностями языка, ведь очевидно, что enum был придуман не для этого.

Потоконебезопасность

Один из популярных вариантов реализации Singleton содержит ленивую инициализацию. Это значит, что объект класса создаётся не в самом начале, а лишь когда будет получено первое обращение к нему. Добиться этого совсем не сложно:

Public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }

Однако здесь начинаются проблемы с потоками, которые могут создавать несколько различных объектов. Происходит это примерно так:

  • Первый поток обращается к getInstance() , когда объект ещё не создан;
  • В это время второй тоже обращается к этому методу, пока первый ещё не успел создать объект, и сам создаёт его;
  • Первый поток создаёт ещё один, второй, экземпляр класса.

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

Есть два способа решить эту проблему. Первый - пометить как synchronised не весь метод, а только блок, где создаётся объект:

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

Не забывайте, что это нельзя использовать в версии Java ниже, чем 1.5, потому что там используется иная модель памяти. Также не забудьте пометить поле instance как volatile .

Второй путь - использовать паттерн “Lazy Initialization Holder”. Это решение основано на том, что вложенные классы не инициализируются до первого их использования (как раз то, что нам нужно):

Public class Singleton { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.instance; } private static class SingletonHolder { private static final Singleton instance = new Singleton(); } }

Рефлексия

Мы запрещаем создавать несколько экземпляров класса, помечая конструктор приватным. Тем не менее, используя рефлексию, можно без особого труда изменить видимость конструктора с private на public прямо во время исполнения:

Class clazz = Singleton.class; Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true);

Конечно, если вы используете Singleton только в своём приложении, переживать не о чем. А вот если вы разрабатываете модуль, который затем будет использоваться в сторонних приложениях, то из-за этого могут возникнуть проблемы. Какие именно, зависит от того, что делает ваш “Одиночка” - это могут быть как и риски, связанные с безопасностью, так и просто непредсказуемое поведение модуля.

Заключение

Несмотря на то, что паттерн Singleton очень известный и популярный, у него есть множество серьёзных недостатков. Чем дальше, тем больше этих недостатков выявляется, и оригинальные паттерны из книги GOF “Design Patterns” часто сегодня считаются антипаттернами. Тем не менее, сама идея иметь лишь один объект на класс по-прежнему имеет смысл, но достаточно сложно реализовать ее правильно.

Паттерн Singleton появился, пожалуй, как только появились статичные объекты. В Smalltalk-80 так был сделан ChangeSet, а чуть в самых разных библиотеках стали появляться сессии, статусы и тому подобные объекты, которых объединяло одно - они должны были быть одни-единственные на всю программу.

В 1994 году вышла известная книга «Паттерны проектирования», представив публике, среди 22-х прочих, и нашего героя, которого теперь назвали Singleton. Была там и его реализация на C++, вот такая:

//.h class Singleton { public: static Singleton* Instance(); protected: Singleton(); private: static Singleton* _instance; } //.cpp Singleton* Singleton::_instance = 0; Singleton* Singleton::Instance() { if(_instance == 0){ _instance = new Singleton; } return _instance; }
Что касается потоков, авторы про них даже и не пишут, считая эту проблему малоактуальной. Зато много внимания уделили всяким тонкостям наследования таких классов друг от друга.

Ничего удивительного - на дворе был 1995 год и многозадачные операционные системы были слишком медленными, чтобы кого-то смутить.

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

В 1995 году Скотт Майерс выпускает свою вторую книгу о хитростях C++. Среди прочего, он призывает в ней использовать Singleton вместо статичных классов - чтобы экономить память и точно знать, когда выполнится его конструктор.

Именно в этой книге появился каноничный синглтон Майерса и я не вижу причин, чтобы не привести его здесь:
class singleton { public: static singleton* instance() { static singleton inst; return &inst; } private: singleton() {} };

Аккуратно, лаконично и умело обыгран стандарт языка. Локальная статичная переменная в функции будет вызвана тогда и только тогда, когда будет вызвана сама функция.

Потом его расширили, запретив чуть больше операций:

Class CMySingleton { public: static CMySingleton& Instance() { static CMySingleton singleton; return singleton; } // Other non-static member functions private: CMySingleton() {} // Private constructor ~CMySingleton() {} CMySingleton(const CMySingleton&); // Prevent copy-construction CMySingleton& operator=(const CMySingleton&); // Prevent assignment };
Согласно новому стандарту C++11, больше для поддержки потоков ничего и не надо. Но до полной его поддержки всеми компиляторами надо ещё дожить.

А пока вот уже не меньше полутора десятков лет лучшие умы пытались поймать многопоточный singleton в клетку языкового синтаксиса. C++ не поддерживал потоки без сторонних библиотек - так что очень скоро почти под каждую библиотеку с потоками появился свой Singleton, который был «лучше всех прочих». Александреску уделяет им целую главу, отечественные разработчики борются с ним не на жизнь, а на смерть, а некто Андрей Насонов тоже долго экспериментирует и в итоге предлагает… совершенно другое решение.

В 2004 Мейерс и Александреску объединили усилия и описали Singleton с Double-check locking. Идея проста - если синглтон не обнаружен в первом if-е, делаем lock, и уже внутри проверяем ещё раз.

А пока суд да дело, проблема потоково-безопасного Singleton переползла и на прочие C-подобные языки. Сперва - на Java, а затем и на C#. И вот уже Джон Скит предлагает целый набор решений, у каждого из которых есть и плюсы, и минусы. И их же предлагает Microsoft.

Для начала - тот самый вариант с double-check locking. Microsoft советует записывать его вот так:
using System; public sealed class Singleton { private static volatile Singleton instance; private static object syncRoot = new Object(); private Singleton() {} public static Singleton Instance { get { if (instance == null) { lock (syncRoot) { if (instance == null) instance = new Singleton(); } } return instance; } } }

Скит, однако, считает, что этот код плох. Почему?

Это не работает в Java. Модель памяти Java до версии 1.5 не проверяла, завершилось ли выполнение конструктора прежде, чем присвоить значение. К счастью, это уже не актуально - давно вышла Java 1.7, а Microsoft рекомендует этот код и гарантирует, что он будет работать.
- Его легко поломать. Запутаешься в скобках - и всё.
- Из-за lock-а он достаточно медлителен
- Есть лучше

Были и варианты без использования потоковых интерфейсков.

В частности, известная реализация через readonly поле. По мнению Скита (и Microsoft), это первый заслуживающий внимания вариант: Вот как он выглядит:

Public sealed class Singleton { private static readonly Singleton instance = new Singleton(); // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Singleton() { } private Singleton() { } public static Singleton Instance { get { return instance; } } }

Этот вариант тоже thread-safe и основан на любопытном свойстве полей readonly - они иницализируются не сразу, а при первом вызове. Замечательная идея, и сам автор рекомендует использовать именно её.

Есть ли у этой реализации недостатки? Разумеется, да:

Если у класса есть статичные методы, то при их вызове readonly поле инициализируется автоматически.
- Конструктор может быть только статичным. Это особенность компилятора - если конструткор не статичен, то тип будет помечен как beforefieldinit и readonly создадутся одновременно со static-ами.
- Статичные конструкторы нескольких связанных Singleton-ов могут нечаянно зациклить друг друга, и тогда уже ничто не поможет и никто не спасёт.

Наконец, известнейшая lazy-реализация с nested-классом.
public sealed class Singleton { private Singleton() { } public static Singleton Instance { get { return Nested.instance; } } private class Nested { // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Nested() { } internal static readonly Singleton instance = new Singleton(); } }

Недостатки у него те же самые, что у любого другого кода, который использует nested-классы.

В последних версиях C# появился класс System.Lazy, который всё это инкупсулирует. А значит, реализация стала ещё короче:
public sealed class Singleton { private static readonly Lazy lazy = new Lazy(() => new Singleton()); public static Singleton Instance { get { return lazy.Value; } } private Singleton() { } }

Легко заметить, что и реализации с readonly, и вариант с nested-классом, и его упрощение в виде lazy объекта не работают с потоками. Вместо этого они используют сами структуры языка, которые «обманывают» интерпретатор. В этом их важнейшее отличие от double-lock"а, который работает именно с потоками.

Почему нехорошо «обманывать» язык? Потому что каждый такой «хак» очень легко нечаянно поломать. И потому что от него нет никакой пользы людям, которые пишут на других языках - а ведь паттерн предполагает универсальность.

Лично я считаю, что проблему потоков надо решать стандартными средствами. В C# встроено множество классов и целые ключевые слова для работы с многопоточностью. Почему бы не использовать стандартные средства, вместо того, чтобы пытаться «обмануть» компилятор.

Как я уже сказал, lock - не лучшее решение. Дело в том, что компилятор разворачивает вот такой lock(obj):

Lock(this) { // other code }

Примерно в такой код:

Boolean lockTaken = false; try { Monitor.Enter(this, ref lockTaken); // other code } finally { if(lockTaken) Monitor.Exit(this); }

Джеффри Рихтер считает этот код весьма неудачным. Во-первых, try - это очень медленно. Во-вторых, если try рухнул, то в коде что-то не то. И когда второй поток начнёт его выполнять, то ошибка скорее всего повторится. Поэтому он призывает использовать для обычных потоков Monitor.Enter / Monitor.Exit, а Singleton переписать на атомарных операциях. Вот так:

Public sealed class Singleton { private static readonly Object s_lock = new Object(); private static Singleton instance = null; private Singleton() { } public static Singleton Instance { get { if(instance != null) return instance; Monitor.Enter(s_lock); Singleton temp = new Singleton(); Interlocked.Exchange(ref instance, temp); Monitor.Exit(s_lock); return instance; } } }

Временная переменная нужна, потому что стандарт C# требует от компилятора сначала создавать переменную, а потом его присваивать. В итоге может получиться так, что в instance уже не null, но инициализация singleton-а ещё не завершена. См. описание подобный случаев в 29-й главе CLR via C# Джеффри Рихтера, раздел The Famous Double-Check Locking Technique .

Таким образом, нашлось место и double-lock-у

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