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

Введение

Шаблон Одиночка на Java является одним из шаблонов проектирования Банды Четырех и относится к категории Шаблонов Создания. Судя по определению, он кажется простым шаблоном проектирования, но при реализации возникает множество вопросов.

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

Принципы шаблона Одиночка

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

Реализация шаблона Singleton в Java

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

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

В последующих разделах мы рассмотрим различные подходы к реализации шаблона Singleton и проблемы проектирования при его реализации.

1. Стремительная инициализация

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

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // частный конструктор, чтобы избежать использования клиентскими приложениями конструктора
    private EagerInitializedSingleton(){}

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

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

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

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

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // инициализация статического блока для обработки исключений
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

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

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

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

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

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 одновременно. Это нарушит паттерн Singleton, и оба потока получат разные экземпляры класса Singleton. В следующем разделе мы рассмотрим различные способы создания потокобезопасного класса Singleton.

4. Однопоточный Синглтон

A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:

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;
    }

}

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

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

Продолжайте обучение с Классом Синглтона с Потокобезопасностью.

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

Перед Java 5 у модели памяти Java было много проблем, и предыдущие подходы терпели неудачу в определенных сценариях, когда слишком много потоков одновременно пытались получить экземпляр класса Singleton. Так что Bill Pugh предложил другой подход для создания класса Singleton с использованием внутреннего статического вспомогательного класса. Вот пример реализации Singleton от Bill Pugh:

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;
    }
}

Обратите внимание на частный внутренний статический класс, который содержит экземпляр класса Singleton. Когда класс Singleton загружается, класс SingletonHelper не загружается в память, и только когда кто-то вызывает метод getInstance(), этот класс загружается и создает экземпляр класса Singleton. Это самый распространенный подход к реализации класса Singleton, поскольку он не требует синхронизации.

6. Использование Reflection для разрушения шаблона Singleton

Reflection можно использовать для разрушения всех предыдущих подходов к реализации Singleton. Вот пример класса:

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) {
                // Этот код разрушит шаблон Singleton
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

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

7. Перечисление Singleton

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

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // сделать что-то
    }
}

8. Сериализация и Singleton

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

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();

        // десериализация из файла в объект
        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());

    }

}

Этот код приводит к следующему результату:

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

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

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

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

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

Заключение

В этой статье рассмотрен шаблон проектирования одиночка.

Продолжайте свое обучение с дополнительными учебниками по Java.

Source:
https://www.digitalocean.com/community/tutorials/java-singleton-design-pattern-best-practices-examples