Шаблон проектирования «синглтон» в языке Java

boat-336551_1280

Шаблон «синглтон» (singleton) — это проектное решение, в котором приложение желает иметь один единственный экземпляр любого из классов, во всех возможных сценариях, без каких-либо исключений. Среди Java-разработчиков довольно долго шли споры о возможных подходах к тому, как сделать любой класс синглтоном. И вы по-прежнему можете встретить людей, которых не устроит ни одно из предложенных вами решений. Но и их мнение нельзя не учитывать. В этой статье мы обсудим несколько грамотных подходов и постараемся сделать всё, на что мы способны.

Разделы этой статьи:

  • Ранняя инициализация
  • Ленивая инициализация
  • Инициализация статическими блоками
  • Решение Билла Пью
  • Использование Enum
  • Добавление readResolve()
  • Добавление серийного идентификатора версии
  • Вывод

Термин «синглтон» произошел от его математического аналога — одноэлементного множества. Как уже говорилось выше, он требует, чтобы у нас был всего один экземпляр. Давайте взглянем на возможные варианты:

Ранняя инициализация

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

public class EagerSingleton {
    private static volatile EagerSingleton instance = new EagerSingleton();
    // private constructor
    private EagerSingleton() {

    }

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

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

Теперь давайте решим эту же задачу следующим методом.

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

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

public final class LazySingleton {
    private static volatile LazySingleton instance = null; 
    // private constructor
    private LazySingleton() {
    }

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

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

Но и у этого метода есть свои недостатки. Давайте взглянем на них. Предположим, что есть два потока Т1 и Т2. Оба потока доходят до создания экземпляра и выполняют команду “instance==null”, теперь каждый из потоков идентифицировал переменную экземпляра как null, таким образом, полагая, что ему необходимо создать экземпляр. Один за другим они переходят в синхронизированный блок и создают экземпляры. В итоге в нашем приложении появляются два экземпляра.

Для исправления этой ошибки можно использовать блокировку с двойной проверкой (double-checked locking). Этот принцип требует от нас перепроверять переменную образца в синхронизированном блоке повторно, делается это следующим образом:

public class EagerSingleton {
    private static volatile EagerSingleton instance = null;
    // private constructor
    private EagerSingleton() {
    }
    public static EagerSingleton getInstance() {
        if (instance == null) {
            synchronized (EagerSingleton.class) {
                // Double check
                if (instance == null) {
                    instance = new EagerSingleton();
                }
            }
        }
        return instance;
      }
}

Представленный выше код является корректной реализацией шаблона синглтон.

Не забудьте использовать ключевое слово “volatile” вместе с переменной экземпляра. В противном случае вы можете столкнуться с ошибкой записи, когда ссылка на экземпляр возвращается раньше, чем объект создается на самом деле. То есть, JVM успела только выделить участок памяти, но код конструктора еще не был выполнен. В этом случае второй поток, который ссылается на неинициализированный объект, может выдать исключение о нулевом указателе и даже вызвать сбой всего приложения.

Инициализация статическими блоками

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

public class StaticBlockSingleton {
    private static final StaticBlockSingleton INSTANCE;
    static {
        try {
            INSTANCE = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Ух, этого я не ожидал!", e);
        }
    }
    public static StaticBlockSingleton getInstance() {
        return INSTANCE;
    }
    private StaticBlockSingleton() {
        // ...
    }
}

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

В следующем разделе мы научимся обходить эту проблему.

Решение Билла Пью

Билл Пью прикладывал наибольшее количество усилий для внесения изменений в модель памяти Java. Его принцип “идиома владельца для инициализации по запросу” тоже использует статичный блок, но иным образом. Он рекомендует использование статичного внутреннего класса.

public class BillPughSingleton {
    private BillPughSingleton() {
    }

    private static class LazyHolder {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

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

Использование Enum

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

public enum EnumSingleton {
    INSTANCE;
    public void someMethod(String param) {
        // какой-нибудь член класса
    }
}

Добавление readResolve()

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

Вот наш класс-синглтон:

public class DemoSingleton implements Serializable {
    private volatile static DemoSingleton instance = null;
    public static DemoSingleton getInstance() {
        if (instance == null) {
            instance = new DemoSingleton();
        }
        return instance;
    }
    private int i = 10;
    public int getI() {
        return i;
    }
    public void setI(int i) {
        this.i = i;
    }
}

Давайте проведем сериализацию этого класса и десериализуем его после внесения некоторых изменений:

public class SerializationTest {
    static DemoSingleton instanceOne = DemoSingleton.getInstance();
    public static void main(String[] args) {
        try {
            // Сериализация в файл
            ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                    "filename.ser"));
            out.writeObject(instanceOne);
            out.close();
            instanceOne.setI(20);
            // Сериализация в файл
            ObjectInput in = new ObjectInputStream(new FileInputStream(
                    "filename.ser"));
            DemoSingleton instanceTwo = (DemoSingleton) in.readObject();
            in.close();
            System.out.println(instanceOne.getI());
            System.out.println(instanceTwo.getI());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
Результат:
20
10

К сожалению, у обеих переменных значения переменной ‘i’ отличаются. Очевидно, что это два экземпляра нашего класса. И, значит, мы опять столкнулись с проблемой наличия нескольких экземпляров в одном приложении. Для решения этого вопроса нам следует включить метод readResolve() в наш класс DemoSingleton. Этот метод будет вызываться при десериализации объекта. Внутри этого метода мы должны возвращать существующий экземпляр, чтобы гарантировать, что во всем приложении он будет только один.

public class DemoSingleton implements Serializable {
    private volatile static DemoSingleton instance = null;
    public static DemoSingleton getInstance() {
        if (instance == null) {
            instance = new DemoSingleton();
        }
        return instance;
    }
    protected Object readResolve() {
        return instance;
    }
    private int i = 10;
    public int getI() {
        return i;
    }
    public void setI(int i) {
        this.i = i;
    }
}

Теперь при выполнении метода SerializationTest этого класса, вы увидите правильные результаты.

Добавление идентификатора серийной версии

Пока всё идет по плану. До этого момента мы одновременно решали и проблему синхронизации, и проблему сериализации. Теперь мы стоим всего в одном шаге от правильной и полной реализации нашей идеи. И не хватает нам только идентификатора серийной версии.

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

java.io.InvalidClassException: singleton.DemoSingleton;
local class incompatible: stream classdesc serialVersionUID = 5026910492258526905,
local class serialVersionUID = 3597984220566440782
at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
at java.io.ObjectInputStream.readClassDesc(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at singleton.SerializationTest.main(SerializationTest.java:24)

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

Вывод

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

public class DemoSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private DemoSingleton() {
        // private constructor
    }
    private static class DemoSingletonHolder {
        public static final DemoSingleton INSTANCE = new DemoSingleton();
    }
    public static DemoSingleton getInstance() {
        return DemoSingletonHolder.INSTANCE;
    }
    protected Object readResolve() {
        return getInstance();
    }
}

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

Приятной учебы!

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

java.awt.Desktop#getDesktop()
java.lang.Runtime#getRuntime()

Оригинал

Комментарии:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>