JavaRush/Java блог/Java Developer/Паттерны проектирования: Singleton
Автор
Артем Divertitto
Senior Android-разработчик в United Tech

Паттерны проектирования: Singleton

Статья из группы Java Developer
участников
Привет! Сегодня будем подробно разбираться в разных паттернах проектирования, и начнем с шаблона Singleton, который еще называют “одиночка”. Паттерны проектирования: Singleton - 1Давай вспомним: что мы знаем о шаблонах проектирования в целом? Шаблоны проектирования — это лучшие практики, следуя которым можно решить ряд известных проблем. Шаблоны проектирования как правило не привязаны к какому-либо языку программирования. Воспринимай их как свод рекомендаций, следуя которым можно избежать ошибок и не изобретать свой велосипед.

Что такое синглтон?

Синглтон — это один из самых простых шаблонов (паттернов) проектирования, который применяется к классу. Иногда говорят: “этот класс — синглтон”, подразумевая, что этот класс реализует паттерн проектирования синглтон. Иногда необходимо написать класс, у которого можно будет создать только один объект. Например, класс, отвечающий за логирование или подключение к базе данных. Шаблон проектирования синглтон описывает, как мы можем выполнить такую задачу. Синглтон — это шаблон (паттерн) проектирования, который делает две вещи:
  1. Дает гарантию, что у класса будет всего один экземпляр класса.

  2. Предоставляет глобальную точку доступа к экземпляру данного класса.

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

  2. Публичный статический метод, который возвращает экземпляр класса. Данный метод называют getInstance. Это глобальная точка доступа к экземпляру класса.

Варианты реализации

Шаблон проектирования синглтон применяют по-разному. Каждый вариант по-своему хорош и плох. Тут как всегда: идеала нет, но нужно к нему стремиться. Но прежде всего давай определимся, что такое хорошо и что такое плохо, и какие метрики влияют на оценку реализации шаблона проектирования. Начнем с положительного. Вот критерии, которые придают реализации сочности и привлекательности:
  • Ленивая инициализация: когда класс загружается во время работы приложения именно тогда, когда он нужен.

  • Простота и прозрачность кода: метрика, конечно, субъективная, но важная.

  • Потокобезопасность: корректная работа в многопоточной среде.

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

Теперь минусы. Перечислим критерии, которые выставляют реализацию в нелучшем свете:
  • Не ленивая инициализация: когда класс загружается при старте приложения, независимо от того, нужен он или нет (парадокс, в мире IT лучше быть лентяем)

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

  • Отсутствие потокобезопасности. Иными словами, “потокоопасность”. Некорректная работа в многопоточной среде.

  • Низкая производительность в многопоточной среде: потоки блокируют друг друга все время либо часто, при совместном доступе к ресурсу.

Код

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

Simple Solution

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
Самая простая реализация. Плюсы:
  • Простота и прозрачность кода

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

  • Высокая производительность в многопоточной среде

Минусы:
  • Не ленивая инициализация.
В попытке исправить последний недостаток, мы получаем реализацию номер два:

Lazy Initialization

public class Singleton {
  private static Singleton INSTANCE;

  private Singleton() {}

  public static Singleton getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
    return INSTANCE;
  }
}
Плюсы:
  • Ленивая инициализация.

Минусы:
  • Не потокобезопасно

Реализация интересна. Мы можем инициализироваться лениво, но утратили потокобезопасность. Не беда: в реализации номер три мы все синхронизируем.

Synchronized Accessor

public class Singleton {
  private static Singleton INSTANCE;

  private Singleton() {
  }

  public static synchronized Singleton getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
    return INSTANCE;
  }
}
Плюсы:
  • Ленивая инициализация.

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

Минусы:
  • Низкая производительность в многопоточной среде

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

Double Checked Locking

public class Singleton {
    private static Singleton INSTANCE;

  private Singleton() {
  }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}
Плюсы:
  • Ленивая инициализация.

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

  • Высокая производительность в многопоточной среде

Минусы:
  • Не поддерживается на версиях Java ниже 1.5 (в версии 1.5 исправили работу ключевого слова volatile)

Отмечу, что для корректной работы данного варианта реализации обязательно одно из двух условий. Переменная INSTANCE должна быть либо final, либо volatile. Последняя реализация, которую мы сегодня обсудим, — Class Holder Singleton.

Class Holder Singleton

public class Singleton {

   private Singleton() {
   }

   private static class SingletonHolder {
       public static final Singleton HOLDER_INSTANCE = new Singleton();
   }

   public static Singleton getInstance() {
       return SingletonHolder.HOLDER_INSTANCE;
   }
}
Плюсы:
  • Ленивая инициализация.

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

  • Высокая производительность в многопоточной среде.

Минусы:
  • Для корректной работы необходима гарантия, что объект класса Singleton инициализируется без ошибок. Иначе первый вызов метода getInstance закончится ошибкой ExceptionInInitializerError, а все последующие NoClassDefFoundError.

Реализация практически идеальная. И ленивая, и потокобезопасная, и быстрая. Но есть нюанс, описанный в минусе. Сравнительная таблица различных реализаций паттерна Singleton:
Реализация Ленивая инициализация Потокобезопасность Скорость работы при многопоточности Когда использовать?
Simple Solution - + Быстро Никогда. Либо когда не важна ленивая инициализация. Но лучше никогда.
Lazy Initialization + - Неприменимо Всегда, когда не нужна многопоточность
Synchronized Accessor + + Медленно Никогда. Либо когда скорость работы при многопоточности не имеет значения. Но лучше никогда
Double Checked Locking + + Быстро В редких случаях, когда нужно обрабатывать исключения при создании синглтона. (когда неприменим Class Holder Singleton)
Class Holder Singleton + + Быстро Всегда, когда нужна многопоточность и есть гарантия, что объект синглтон класса будет создан без проблем.

Плюсы и минусы паттерна Singleton

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

  2. Предоставляет глобальную точку доступа к экземпляру данного класса.

Однако у этого шаблона есть недостатки:
  1. Синглтон нарушает SRP (Single Responsibility Principle) — класс синглтона, помимо непосредственных обязанностей, занимается еще и контролированием количества своих экземпляров.

  2. Зависимость обычного класса или метода от синглтона не видна в публичном контракте класса.

  3. Глобальные переменные это плохо. Синглтон превращается в итоге в одну здоровенную глобальную переменную.

  4. Наличие синглтона снижает тестируемость приложения в целом и классов, которые используют синглтон, в частности.

Ну вот и все. Мы рассмотрели с тобой паттерн проектирования синглтон. Теперь в разговоре за жизнь с друзьями программистами ты сможешь сказать не только чем он хорош, но и пару слов о том, чем он плох. Удачи в освоении новых знаний.

Дополнительное чтение:

Комментарии (31)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Ислам
Уровень 33
9 июля 2023, 12:52
Nice
ivan дворник
22 июня 2023, 15:01
что за за бред с ссылкой Singleton "On Demand Holder idiom"? почему у меня открывается онлайн казино
SoSed
Уровень 51
Expert
30 января 2023, 21:27
"Отмечу, что для корректной работы данного варианта реализации обязательно одно из двух условий. Переменная INSTANCE должна быть либо final, либо volatile. " При этом, в примере кода, переменная не final и неvolatile))
dvazhdydva
Уровень 22
2 ноября 2022, 10:15
в статье на хабре написано: 1) Использовать нормальную (не ленивую) инициализацию везде где это возможно;, а здесь: Когда использовать? Никогда. Либо когда не важна ленивая инициализация. Но лучше никогда. кто прав?
Igor Makarov
Уровень 30
27 ноября 2022, 07:50
вероятно в до определенного уровня разработки (скажем senior) тебе можно использовать нормальный сингл тон и не париться, а дальше, так как ты специалист и т.п. использую наиболее подходящий вариант
GromStal
Уровень 33
5 апреля, 21:37
в реальной жизни создавать синглтоны самому не нужно. Есть Spring
Roman Eroshenko Доходяга в Помойка
23 сентября 2022, 13:41
Высокая производительность в многопоточной среде: потоки блокируют друг друга минимально, либо вообще не блокируют при совместном доступе к ресурсу. Теперь минусы. Перечислим критерии, которые выставляют реализацию в нелучшем свете: Низкая производительность в многопоточной среде: потоки блокируют друг друга все время либо часто, при совместном доступе к ресурсу. Не понял прикола
Rostik
Уровень 36
20 апреля 2022, 13:43
Касаемо раздела статьи "Double Checked Locking". Автор забыл обязательное в данном случае ключевое слово volatile. Смотрим, почему. Да и в принципе, guys, советую целиком чекнуть данное видео. Довольно чётко & ясно про азы multithreading.👌
CyberBoar
Уровень 39
12 апреля 2022, 06:18
И как в последнем случае нам гарантировать, что объект инициализируется без ошибок?
Denis
Уровень 33
22 декабря 2021, 04:47
Я запутался... Эта статья и другие источники рассказывают нам о том, что в JVM реализована "ленивая загрузка классов", т.е. загрузка классов происходит не сразу, а при первом обращении к ним. Так же я помню из других источников, что инициализация статических полей и статических блоков класса выполняется ОДИН РАЗ ПРИ ПЕРВОМ ОБРАЩЕНИИ К КЛАССУ. Получается, что если разработчик в своём коде не обращается к объекту класса Singletone, то и статические поля и статические блоки выполнятся не будут, а значит и не будет создан экземпляр класса. Тогда единственный минус первого примера как бы отпадает - верно? А значит первый вариант - самый простой и правильный вариант паттерна Singletone. Кто может объяснить мне, почему в статье в самом первом примере указан минус паттерна "Не ленивая инициализация"?
fedyaka
Уровень 36
12 января 2022, 16:26
На сколько я понимаю, здесь скорей Simple Solution непотокобезопасное, из за того что к классу может обратиться несколько потоков одновременно и начать создавать новые объекты, и записывать их в переменную(ошибка, ведь это переменная final, перезаписать нельзя). Либо пока первый поток будет создавать объект, другой увидя что объект есть(но ещё не доделаный), попробует его достать и получит непонятную консистенцию. Ведь ничего не мешает им это сделать, синхронизации никакой нет.🤔 Но это лишь мои догадки, утверждать не могу.
Игорь Full Stack Developer в IgorApplications
9 августа 2021, 12:35
Никого не смущает последний - Class Holder Singleton, якобы лучший пример? Зачем понадобился вложенный класс, если получается тоже самое, что и в самом первом примере.
Игорь
Уровень 33
13 октября 2021, 05:53
Я так понимаю у первого варианты минус отсутсвие ленивой инициализации(своими словами, в первом случае объект инициализируется в момент компиляции). В последнем варианте, инициализация происходит в момент вызова метода getInstance, статический объект класса в данном случае создаётся в момент вызова из публичного. Также почитав об этом паттерне... По сути сами разработчики Java рекомендуют создавать этот паттерн через enum public enum Singleton{ INSTANCE }
19 июля 2021, 06:25
if (INSTANCE == null) INSTANCE = new SingletonExample1();

        return INSTANCE;
Так Singleton работает правильно а если написать вот так
return (INSTANCE == null) ? new SingletonExample1() : INSTANCE;
то создается новый объект каждый раз SingletonDemo{ example1=Singleton.SingletonExample1@67b64c45 } SingletonDemo{ example1=Singleton.SingletonExample1@4411d970 } SingletonDemo{ example1=Singleton.SingletonExample1@6442b0a6 } SingletonDemo{ example1=Singleton.SingletonExample1@60f82f98 } По факту суть кода одна но результат разный. Есть люди кто разбирался в этом? Я че то догнать не могу
26 августа 2021, 12:58
Где во втором случае тот момент когда, если INSTANCE == null, то мы инициализируем ее объектом SingletonExample1?
26 августа 2021, 14:13
Догнал) Во втором случае мы возвращаем новый объект, но не сохраняем ссылку на него в INSTANCE. Поэтому INSTANCE каждый раз будет null и всегда будет создаваться новый объект