Рубрики
Без рубрики

Лучшие практики проектирования одноэлементных шаблонов Java с примерами

Шаблон одноэлементного проектирования Java является наиболее спорным шаблоном проектирования. Как создать одноэлементный класс на java, Потокобезопасность, перечисление, Билл Пью и многое другое.

Автор оригинала: Pankaj Kumar.

Шаблон Java Singleton является одним из Групп из четырех шаблонов проектирования и входит в категорию Шаблон креативного дизайна .

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

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

Одноэлементный Шаблон

  • Одноэлементный шаблон ограничивает создание экземпляра класса и гарантирует, что в виртуальной машине java существует только один экземпляр класса.
  • Одноэлементный класс должен предоставить глобальную точку доступа для получения экземпляра класса.
  • Одноэлементный шаблон используется для ведения журнала , объектов драйверов, кэширования и пула потоков .
  • Шаблон одноэлементного дизайна также используется в других шаблонах дизайна, таких как Абстрактная фабрика , Конструктор , Прототип , Фасад и т.д.
  • Шаблон одноэлементного проектирования также используется в основных классах java, например java.lang.Runtime , java.awt.Desktop .

Реализация Одноэлементного Шаблона Java

Для реализации одноэлементного шаблона у нас есть разные подходы, но все они имеют следующие общие концепции.

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

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

  1. Нетерпеливая инициализация
  2. Инициализация статического блока
  3. Ленивая Инициализация
  4. Потокобезопасный Синглтон
  5. Реализация Билла Пью Синглтона
  6. Использование отражения для уничтожения одноэлементного шаблона
  7. Перечисление Синглтона
  8. Сериализация и Синглтон

1. Нетерпеливая инициализация

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

Вот реализация одноэлементного класса статической инициализации.

package com.journaldev.singleton;

public class EagerInitializedSingleton {
    
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
    
    //private constructor to avoid client applications to use constructor
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance(){
        return instance;
    }
}

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

2. Инициализация статического блока

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

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;
    
    private StaticBlockSingleton(){}
    
    //static block initialization for exception handling
    static{
        try{
            instance = new StaticBlockSingleton();
        }catch(Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    
    public static StaticBlockSingleton getInstance(){
        return instance;
    }
}

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

Читать : Статическая Java

3. Ленивая Инициализация

Метод ленивой инициализации для реализации одноэлементного шаблона создает экземпляр в методе глобального доступа. Вот пример кода для создания одноэлементного класса с помощью этого подхода.

package com.journaldev.singleton;

public class LazyInitializedSingleton {

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

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

4. Потокобезопасный Синглтон

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

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton(){}
    
    public static synchronized ThreadSafeSingleton getInstance(){
        if(instance == null){
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
    
}

Вышеописанная реализация отлично работает и обеспечивает потокобезопасность, но снижает производительность из-за затрат, связанных с синхронизированным методом, хотя она нужна нам только для первых нескольких потоков, которые могут создавать отдельные экземпляры (Читайте: Синхронизация Java ). Чтобы избежать этих дополнительных накладных расходов каждый раз, используется принцип двойной проверки блокировки . При таком подходе синхронизированный блок используется внутри условия if с дополнительной проверкой, чтобы убедиться, что создан только один экземпляр одноэлементного класса.

Следующий фрагмент кода обеспечивает реализацию блокировки с двойной проверкой.

public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
    if(instance == null){
        synchronized (ThreadSafeSingleton.class) {
            if(instance == null){
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

Прочитайте : Потокобезопасный Одноэлементный класс

5. Реализация Билла Пью Синглтона

До Java 5 в модели памяти java было много проблем, и описанные выше подходы приводили к сбоям в определенных сценариях, когда слишком много потоков пытались получить экземпляр одноэлементного класса одновременно. Поэтому Билл Пью предложил другой подход к созданию одноэлементного класса с использованием внутреннего статического вспомогательного класса . Реализация Билла Пью Синглтона выглядит следующим образом;

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}
    
    private static class SingletonHelper{
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }
    
    public static BillPughSingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
}

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

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

Читать : Вложенные классы Java

6. Использование отражения для уничтожения одноэлементного шаблона

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

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                //Below code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

Когда вы запустите приведенный выше тестовый класс, вы заметите, что хэш-код обоих экземпляров не совпадает, что разрушает шаблон одноэлементного кода. Отражение очень мощно и используется во многих фреймворках, таких как Spring и Hibernate, ознакомьтесь с Учебником по отражению Java .

7. Перечисление Синглтонов

Чтобы преодолеть эту ситуацию с помощью размышлений, Джошуа Блох предлагает использовать перечисление для реализации одноэлементного шаблона проектирования, поскольку Java гарантирует, что любое значение перечисления создается только один раз в программе Java. Поскольку значения перечисления Java доступны во всем мире, то же самое относится и к синглету. Недостатком является то, что тип перечисления несколько негибок; например, он не допускает ленивой инициализации.

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;
    
    public static void doSomething(){
        //do something
    }
}

Прочитайте : Перечисление Java

8. Сериализация и синглтон

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

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable{

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}
    
    private static class SingletonHelper{
        private static final SerializedSingleton instance = new SerializedSingleton();
    }
    
    public static SerializedSingleton getInstance(){
        return SingletonHelper.instance;
    }
    
}

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

package com.journaldev.singleton;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();
        
        //deserailize from file to object
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();
        
        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
        
    }

}

Результатом приведенной выше программы является;

instanceOne hashCode=2011117821
instanceTwo hashCode=109647522

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

protected Object readResolve() {
    return getInstance();
}

После этого вы заметите, что хэш-код обоих экземпляров в тестовой программе одинаков.

Прочитайте : Сериализация Java и Десериализация Java .

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