Статьи

Шаблон проектирования Singleton — самоанализ и лучшие практики

Определение:

Singleton является частью шаблона проектирования Gang of Four и относится к шаблонам проектирования. В этой статье мы подробнее рассмотрим использование шаблона Singleton. Это один из самых простых шаблонов проектирования с точки зрения моделирования, но, с другой стороны, это один из самых противоречивых шаблонов с точки зрения сложности использования.
В Java шаблон Singleton гарантирует, что в виртуальной машине Java создается только один экземпляр класса. Он используется для предоставления глобальной точки доступа к объекту. С точки зрения практического использования шаблоны Singleton используются в журналах, кэшах, пулах потоков, настройках конфигурации, объектах драйверов устройств.

Шаблон проектирования часто используется в сочетании с шаблоном проектирования завода . Этот шаблон также используется в шаблоне Service Locator JEE.

Состав:

Диаграмма классов синглтона

Диаграмма классов синглтона

  • Статический член: содержит экземпляр класса singleton.
  • Закрытый конструктор: Это не позволит никому другому создавать экземпляр класса Singleton.
  • Открытый статический метод: обеспечивает глобальную точку доступа к объекту Singleton и возвращает экземпляр клиентскому вызывающему классу.

Пример реализации: Ленивая инициализация

Давайте рассмотрим пример реализации синглтона в Java. В приведенном ниже коде используется процесс инициализации Lazy.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingletonExample {
 
    // Static member holds only one instance of the
    // SingletonExample class
    private static SingletonExample singletonInstance;
 
    // SingletonExample prevents any other class from instantiating
    private SingletonExample() {
    }
 
    // Providing Global point of access
    public static SingletonExample getSingletonInstance() {
        if (null == singletonInstance) {
            singletonInstance = new SingletonExample();
        }
        return singletonInstance;
    }
 
    public void printSingleton(){
        System.out.println('Inside print Singleton');
    }
}
Пояснение кода шаблона синглтона

Пояснение кода шаблона синглтона

Когда этот класс будет вызываться со стороны клиента с помощью SingletonExample.getSingletonInstance (). PrintSingleton (); тогда в первый раз будет создан только экземпляр. Во второй раз для всех последующих вызовов мы будем ссылаться на один и тот же объект, а метод getSingletonInstance () возвращает тот же экземпляр класса SingletonExample, который был создан в первый раз. Вы можете проверить это, добавив оператор печати следующий код:

1
2
3
4
5
6
7
public static SingletonExample getSingletonInstance() {
        if (null == singletonInstance) {
            singletonInstance = new SingletonExample();
            System.out.println('Creating new instance');
        }
        return singletonInstance;
    }

Если теперь мы вызываем класс Singleton из клиентского класса как:

1
2
3
4
SingletonExample.getSingletonInstance().printSingleton();
SingletonExample.getSingletonInstance().printSingleton();
SingletonExample.getSingletonInstance().printSingleton();
SingletonExample.getSingletonInstance().printSingleton();

Результат вышеупомянутого вызова:

1
2
3
4
5
Creating new instance
Inside print Singleton
Inside print Singleton
Inside print Singleton
Inside print Singleton

Пример реализации: двойная проверка блокировки

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

Синглтон в куче

Синглтон в куче

На рисунке 3 показано, как несколько потоков обращаются к экземпляру singleton. Поскольку экземпляр синглтона является статической переменной класса, хранящейся в пространстве PermGen кучи. Это относится и к экземпляру метода getSingletonInstance (), так как он также является статическим. В среде многопоточности, чтобы каждый поток не создавал другой экземпляр одноэлементного объекта и, таким образом, создавал проблему параллелизма, нам нужно будет использовать механизм блокировки. Это может быть достигнуто синхронизированным ключевым словом. Используя это синхронизированное ключевое слово, мы предотвращаем доступ Thread2 или Thread3 к экземпляру singleton, в то время как Thread1 внутри метода getSingletonInstance () .

С точки зрения кода это означает:

1
2
3
4
5
6
public static synchronized SingletonExample getSingletonInstance() {
        if (null == singletonInstance) {
            singletonInstance = new SingletonExample();
        }
        return singletonInstance;
    }

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

1
singletonInstance = new SingletonExample();

Пример кода:

01
02
03
04
05
06
07
08
09
10
public static volatile SingletonExample getSingletonInstance() {
        if (null == singletonInstance) {
            synchronized (SingletonExample.class){
                    if (null == singletonInstance) {
            singletonInstance = new SingletonExample();
            }
        }
        }
        return singletonInstance;
    }

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

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

Пример реализации: изменчивое ключевое слово

Мы также можем использовать ключевое слово volatile для объявления переменной экземпляра.

1
private volatile static SingletonExample singletonInstance;

Ключевое слово volatile помогает в качестве инструмента управления параллелизмом в многопоточной среде и предоставляет самое последнее обновление самым точным способом. Однако обратите внимание, что двойная проверка блокировки может не работать до Java 5. В такой ситуации мы можем использовать механизм ранней загрузки. Если мы помним об исходном примере кода, мы использовали ленивую загрузку. В случае ранней загрузки мы создадим экземпляр класса SingletonExample в начале, и это будет упоминаться в поле частного статического экземпляра.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class SingletonExample {
    private static final SingletonExample singletonInstance = new SingletonExample;
 
    // SingletonExample prevents any other class from instantiating
    private SingletonExample() {
    }
 
    // Providing Global point of access
    public static SingletonExample getSingletonInstance() {
 
        return singletonInstance;
    }
 
    public void printSingleton(){
        System.out.println('Inside print Singleton');
    }
}

При таком подходе объект-одиночка создается до того, как он понадобится. JVM заботится о инициализации статической переменной и гарантирует, что процесс является потокобезопасным и что экземпляр создается до того, как потоки пытаются получить к нему доступ. В случае отложенной загрузки singletonInstance создается, когда клиентский класс вызывает getSingleInstance (), тогда как в случае ранней загрузки singletonInstance создается, когда класс загружается в память.

Пример реализации: использование enum

Реализация Singleton в Java 5 или более поздней версии с использованием Enum:
Enum является поточно-ориентированным, а реализация Singleton через Enum гарантирует, что ваш синглтон будет иметь только один экземпляр даже в многопоточной среде. Давайте посмотрим на простую реализацию:

01
02
03
04
05
06
07
08
09
10
11
public enum SingletonEnum {
 INSTANCE;
 public void doStuff(){
     System.out.println('Singleton using Enum');
 }
}
And this can be called from clients :
public static void main(String[] args) {
        SingletonEnum.INSTANCE.doStuff();
 
    }

Вопросы и ответы:

Вопрос: Почему мы не можем использовать статический класс вместо синглтона?
Ответ:

  • Одним из ключевых преимуществ singleton по сравнению со статическим классом является то, что он может реализовывать интерфейсы и расширять классы, тогда как статический класс не может (он может расширять классы, но он не наследует их члены-экземпляры). Если мы рассмотрим статический класс, то это может быть только вложенный статический класс, поскольку класс верхнего уровня не может быть статическим классом. Статический означает, что он принадлежит к классу, в котором он находится, а не к какому-либо экземпляру. Так что это не может быть класс высшего уровня.
  • Другое отличие состоит в том, что статический класс будет иметь все свои члены как статические только в отличие от Singleton.
  • Еще одним преимуществом Singleton является то, что он может загружаться лениво, тогда как static будет инициализироваться всякий раз, когда он загружается впервые.
  • Объект Singleton хранится в Heap, но статический объект хранится в стеке.
  • Мы можем клонировать объект Singleton, но мы не можем клонировать объект статического класса.
  • Синглтон может использовать объектно-ориентированную особенность полиморфизма, но статический класс не может.

Вопрос: Может ли синглтон-класс быть подклассом?
Ответ: Откровенно говоря, синглтон — это просто шаблон проектирования, и его можно разделить на подклассы. Однако стоит понимать логику или требования, лежащие в основе подкласса одноэлементного класса, поскольку дочерний класс может не наследовать цель одноэлементного шаблона путем расширения класса Singleton. Однако подклассы можно предотвратить, используя ключевое слово final в объявлении класса.

Вопрос: может ли быть несколько экземпляров синглтона с использованием клонирования?
Ответ: Это был хороший улов! Что же нам теперь делать? Чтобы предотвратить создание другого экземпляра экземпляра-одиночки, мы можем выбросить исключение из метода clone ().

Вопрос: Каково влияние, если мы создаем еще один экземпляр синглтона, используя сериализацию и десериализацию?
Ответ: Когда мы сериализуем класс и десериализуем его, он создает еще один экземпляр класса singleton. Как правило, столько раз, сколько вы десериализуете экземпляр Singleton, он будет создавать несколько экземпляров. Хорошо в этом случае лучший способ состоит в том, чтобы сделать синглтон как enum. Таким образом, базовая реализация Java заботится обо всех деталях. Если это невозможно, нам нужно переопределить метод readobject (), чтобы вернуть тот же экземпляр синглтона.

Вопрос: Какой другой шаблон работает с Singleton?
Ответ: Есть несколько других шаблонов, таких как метод Factory, шаблон компоновщика и прототипа, который использует шаблон Singleton во время реализации.

Вопрос: Какие классы в JDK используют шаблон синглтона?
Ответ: java.lang.Runtime: в каждом приложении Java есть только один экземпляр времени выполнения, который позволяет приложению взаимодействовать со средой, в которой оно выполняется. GetRuntime эквивалентно методу getInstance () одноэлементного класса.

Использование шаблона проектирования Singleton:

Различные варианты использования шаблонов Singleton:

  • Доступ к аппаратному интерфейсу: использование синглтона зависит от требований. Однако практически единый может использоваться в случае, если требуется ограничение использования внешних аппаратных ресурсов, например, аппаратные принтеры, в которых спулер печати может быть выполнен как единый, чтобы избежать множественных одновременных обращений и создания тупиковых ситуаций.
  • Logger: Аналогично, синглтон является хорошим потенциальным кандидатом для использования при генерации файлов журнала. Представьте себе приложение, в котором утилита ведения журнала должна создать один файл журнала на основе сообщений, полученных от пользователей. Если есть несколько клиентских приложений, использующих этот класс утилит ведения журнала, они могут создать несколько экземпляров этого класса, и это может вызвать проблемы при одновременном доступе к одному и тому же файлу журнала. Мы можем использовать служебный класс регистратора в качестве единого элемента и предоставить глобальную точку отсчета.
  • Файл конфигурации. Это еще один потенциальный кандидат на использование шаблона Singleton, поскольку он имеет преимущество в производительности, так как предотвращает многократный доступ и чтение файла конфигурации или файла свойств несколькими пользователями. Он создает один экземпляр файла конфигурации, к которому могут одновременно обращаться несколько вызовов, поскольку он предоставляет статические данные конфигурации, загруженные в объекты в памяти. Приложение только считывает данные из файла конфигурации в первый раз, а после второго вызова клиентские приложения считывают данные из объектов в памяти.
  • Кэш: мы можем использовать кэш как одноэлементный объект, так как он может иметь глобальную точку отсчета, и для всех будущих вызовов к объекту кэша клиентское приложение будет использовать объект в памяти.

Руки вверх:

Давайте возьмем небольшой практический пример для более детального понимания шаблона проектирования Singleton.
Постановка задачи:
Разработайте небольшое приложение для печати банкоматов, которое может генерировать различные типы выписок по транзакции, включая мини-выписку, детальную выписку и т. Д. Однако клиент должен знать о создании этих выписок. Убедитесь, что потребление памяти сведено к минимуму.
Дизайнерское решение:
Вышеупомянутое требование может быть выполнено с использованием двух основных группировок из четырех шаблонов проектирования — шаблон проектирования завода и шаблон проектирования Singleton. Чтобы сгенерировать несколько типов операторов для транзакций ATM на банкомате, мы можем создать объект Statement Factory, который будет иметь фабричный метод createStatements () . CreateStatements создаст объекты DetailStatement или MiniStatement. Клиентский объект полностью не будет знать о создании объекта, поскольку он будет взаимодействовать только с интерфейсом Factory. Мы также создадим интерфейс под названием StatementType. Это позволит добавить дополнительные объекты типа выписки, например выписка по кредитной карте и т. Д. Таким образом, решение является масштабируемым и расширяемым по принципу объектно-ориентированного открытого / закрытого проектирования.
Второе требование снижения потребления памяти может быть достигнуто с помощью шаблона проектирования Singleton. Класс «Фабрика операторов» не нужно запускать несколько раз, и одна фабрика может создавать несколько объектов операторов. Шаблон Singleton создаст один экземпляр класса StatementFactory, тем самым экономя память.

Пример банкомата

Пример банкомата

  • Фабрика: Фабрика — это абстрактный класс, представляющий собой единую точку контакта для клиента. Все конкретные фабричные классы должны реализовывать абстрактный фабричный метод.
  • StatementFactory: это класс реализации Factory, который состоит из метода creator. Этот класс происходит от абстрактного класса Factory. Это основной класс создателя для всех продуктов, например, для операторов.
  • StatementType: это интерфейс продукта, который обеспечивает абстракцию для различных типов продуктов, которые должны быть созданы классом Factory.
  • DetailStatement: это конкретная реализация интерфейса StatementType, который будет печатать подробные операторы.
  • MiniStatement: это конкретная реализация интерфейса StatementType, который будет печатать мини-операторы.
  • Клиент: это класс клиента, который будет вызывать StatementFactory и StatementType и запрашивать создание объекта.

Предположение:
Дизайнерское решение обслуживает только один банкомат.

Образец кода:

Factory.java

1
2
3
public abstract class Factory {
 protected abstract StatementType createStatements(String selection);
 }

StatementFactory.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class StatementFactory extends Factory {
    private static StatementFactory uniqueInstance;
 
    private StatementFactory() {
    }
 
    public static StatementFactory getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new StatementFactory();
            System.out.println('Creating a new StatementFactory instance');
        }
        return uniqueInstance;
 
    }
 
    public StatementType createStatements(String selection) {
        if (selection.equalsIgnoreCase('detailedStmt')) {
            return new DetailedStatement();
        } else if (selection.equalsIgnoreCase('miniStmt')) {
            return new MiniStatement();
        }
        throw new IllegalArgumentException('Selection doesnot exist');
    }
}

StatementType.java

1
2
3
public interface StatementType {
    String print();
}

DetailedStatement.java

1
2
3
4
5
6
7
public class DetailedStatement implements StatementType {
    @Override
    public String print() {
        System.out.println('Detailed Statement Created');
        return 'detailedStmt';
    }
}

MiniStatement.java

1
2
3
4
5
6
7
public class MiniStatement implements StatementType {
    @Override
    public String print() {
        System.out.println('Mini Statement Created');
        return 'miniStmt';
    }
}

Client.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class Client {
 
    public static void main(String[] args) {
        System.out.println('Enter your selection:');
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String selection = null;
        try {
            selection = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Factory factory = StatementFactory.getUniqueInstance();
        StatementType objStmtType = factory.createStatements(selection);
        System.out.println(objStmtType.print());
    }
 
}

Вывод:

В вышеприведенных статьях мы подробно рассмотрели шаблон Singleton, как реализовать шаблон Singleton вместе с практическим применением. Хотя шаблон синглтона выглядит простой реализацией, мы должны воздерживаться от его использования до тех пор, пока к нему не будет предъявляться строгие требования. Вы можете обвинить непредсказуемый характер результатов в многопоточной среде. Хотя мы можем использовать enum в Java 5 и выше, иногда трудно реализовать вашу логику всегда в enum, или может быть устаревший код до Java 5. Надеюсь, нашим читателям понравилась эта статья.

Ссылка: Singleton Design Pattern — самоанализ и лучшие практики от нашего партнера JCG Майнака Госвами в блоге Idiotechie .