Поиск по сайту:

Рекомендации по шаблону проектирования Java Singleton с примерами


Введение

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

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

Принципы одноэлементного шаблона

  • Шаблон Singleton ограничивает создание экземпляра класса и гарантирует, что в виртуальной машине Java существует только один экземпляр класса.
  • Класс-одиночка должен предоставлять глобальную точку доступа для получения экземпляра класса.
  • Шаблон Singleton используется для пула потоков.
  • Шаблон проектирования Singleton также используется в других шаблонах проектирования, например Facade и т. д.
  • Шаблон проектирования Singleton также используется в основных классах Java (например, java.lang.Runtime, java.awt.Desktop).

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

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

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

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

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

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

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // private constructor to avoid client applications using the 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 occurred in creating singleton instance");
        }
    }

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

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

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

}

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

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

Продолжите обучение с классом Thread Safe Singleton.

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

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

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

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

6. Использование Reflection для уничтожения Singleton Pattern

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

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) {
                // This 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());
    }

}

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

7. Одноэлементное перечисление

Чтобы преодолеть эту ситуацию с Reflection, значения Java Enum доступны глобально, как и singleton. Недостатком является то, что тип enum несколько негибкий (например, он не допускает ленивой инициализации).

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

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

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

Иногда в распределенных системах нам нужно реализовать интерфейс Serializable в классе singleton, чтобы мы могли хранить его состояние в файловой системе и извлекать его позже. Вот небольшой одноэлементный класс, который также реализует интерфейс 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();

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

    }

}

Этот код производит этот вывод:

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

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

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

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

Читайте о десериализации Java.

Заключение

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

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