Синглтон является относительно простым шаблоном проектирования, однако он затрагивает большое количество аспектов разработки программного обеспечения, таких как потокобезопасность, ленивая инициализация, особенности вызовов статических конструкторов, принципы единой ответственности и инверсии зависимостей, юнит-тестирование, утечки памяти и другие.
В части 1 мы рассмотрим основные положения касающиеся шаблона, а также разберем сильные и слабые стороны нескольких основных его реализаций.
- Введение
- Реализации Синглтона
- Плюсы и минусы Синглтона
- Синглтон против внедрения зависимостей
- Шаблон Ambient Context
- Синглтон и утечки памяти
- Шаблон Monostate
Введение
Синглтон является обычным классом для которого может быть создан только один объект в пределах жизненного цикла домена приложения.
Синглтон реализуется для тех классов, для которых наличие более одного объекта в приложении может привести к дефектам или к напрасному расходу оперативной памяти. Например, если каждый раз при необходимости залогировать информацию создавать новый объект класса 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.