JavaRush /Java блог /Random UA /Паттерни проектування: Singleton
Professor Hans Noodles
41 рівень

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

Стаття з групи Random UA
Вітання! Сьогодні детально розбиратимемося в різних патернах проектування, і почнемо з шаблону 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. Наявність синглтона знижує тестованість програми загалом і класів, які використовують синглтон, зокрема.

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

Додаткове читання:

Коментарі (1)
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ
Юрій Якимчук Рівень 33
22 листопада 2023
Дякую за статтю