Шаблон проектирования Singleton (одиночка), наиболее рациональные реализации в примерах.

Singleton — это один из шаблонов, описанных в книге «Банда четырех. Шаблоны проектирования», в разделе «Порождающие шаблоны проектирования». Из определения, кажется, что это очень простой шаблон проектирования, но когда доходит дело до реализации сразу всплывает множество проблем. Реализация шаблона Singleton — это очень спорная тема среди разработчиков. В этой статье мы рассмотрим принципы шаблона Singleton, различные способы его реализации и несколько наиболее рациональных способов его использования.

Шаблон Singleton
Шаблон Singleton накладывает ограничения на создание экземпляра класса и гарантирует, что в JVM (виртуальной джава машине) существует только один экземпляр данного класса. Класс Singleton-а должен иметь глобальную точку доступа для получения экземпляра класса. Шаблон используют для логирования, объектов драйверов, кеширования и наборов нитей.

Также Singleton используют в других шаблонах проектирования, таких как «абстрактная фабрика» (Abstract Factory), Строитель (Builder), Прототип (Prototype), Фасад(Facade) и т.д.

Этот шаблон также используется и самой Java в ее ядре, например в java.lang.Runtime и java.awt.Desktop.

Singleton в Java
Существуют несколько различных подходов, реализации шаблона Singleton, но все они имеют общие принципы.
  • Private конструктор — для запрета инициализации экземпляра класса из другого класса через конструктор.
  • Private static переменную того же класса, которая и будет единственным экземпляром этого класса.
  • Public static метод, возвращающий экземпляр класса. Это — глобальная точка доступа для внешнего мира позволяющая получить экземпляр класса Singleton.
В дальнейших главах мы освоим различные подходы реализаций шаблона Singleton, а также проблемы связанные с каждой реализацией.

Ранняя реализация (Eager initialization)
В ранней реализации, экземпляр класса Singleton инициализируется одновременно с загрузкой класса. Это наипростейший вариант создания класса Одиночки, но он имеет недостаток: экземпляр создается в любом случае, даже если им никто так и не воспользуется.
Покажем статическую инициализацию класса.

EagerInitializedSingleton.java
package com.journaldev.singleton;
 
public class EagerInitializedSingleton {
   
  private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
   
  // конструктор private, чтобы не было возможности создать экземпляр класса извне.
  private EagerInitializedSingleton(){}
 
  public static EagerInitializedSingleton getInstance(){
    return instance;
  }
}


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

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

StaticBlockSingleton.java
package com.journaldev.singleton;
 
public class StaticBlockSingleton {
 
  private static StaticBlockSingleton instance;
   
  private StaticBlockSingleton(){}
   
  //блок статической инициализации с возможностью обработки исключительных ситуаций
  static{
    try{
      instance = new StaticBlockSingleton();
    }catch(Exception e){
      throw new RuntimeException("При создании объекта «Singleton» произошла ошибка");
    }
  }
   
  public static StaticBlockSingleton getInstance(){
    return instance;
  }
}

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

Прочитать про: Java static

Ленивая инициализация. (Lazy Initialization)
Ленивая инициализация — способ реализации шаблона Singleton с глобальной методом доступа к экземпляру класса. Вот пример кода для создания класса Singleton с таким подходом.
LazyInitializedSingleton.java
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;
  }
}


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

Потоко-безопасный Синглтон
Самый простой способ создать потоко-безопасный Singleton класс — это синхронизировать глобальный метод доступа при помощи synchronized, таким образом только одна нить сможет использовать данный метод. Общая реализация такого класса приведена ниже.
ThreadSafeSingleton.java
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 проводится дополнительная проверка, для гарантии, что будет создан только один экземпляр класса Singleton.

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

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

Прочитать про:Потоковая безопасность в классах Singleton Java с примерами. (на англ.)

Решение Била Пью (Bill Pugh) для шаблона Singleton
До Java5, к модели памяти java было много вопросов и реализации подходов выше, при определенных условиях, были обречены на провал, в частности когда слишком много нитей пытаются одновременно получить экземпляр класса. Бил Пью придумал другой подход создания Singleton класса, использовав внутренний вспомогательный статический класс. Реализация Била Пью шаблона Singleton выглядит следующим образом:

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

Заметьте, что внутренний вспомогательный статический private класс, содержит реализацию Singleton. Когда основной Singleton класс загружен, класс SingletonHelper еще не загружен в память и только, когда кто-то вызовет метод getInstance этот класс подгружается и создает экземпляр Singleton класса.

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

Прочитать про: Вложенные Классы Java (на англ.)

Использование рефлексии (reflection) для разрушения шаблона Singleton.
Рефлексия может разрушить все описанные выше подходы реализации шаблона. Давайте посмотрим это на примере класса.

ReflectionSingletonTest.java
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());
  }
 

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

Рефлексия – это очень мощная техника и используется многими фреймворками такими как Spring, Hibernate. С рефлексией подробней можно ознакомиться тут Java Reflection Tutorial (на англ.).

Singleton с помощью Enum
Для преодоления этой ситуации с рефлексией Joshua Bloch рекомендует использовать Enum для реализации шаблона Singleton, только так java гарантирует, что любое Enum значение имеет только один экземпляр в программе Java. С тех пор как Enum значения могут иметь глобальный доступ они используются в Singleton. Недостатком является то, что возвращаемый Enum тип, несколько негибкий, например, не поддерживает ленивую инициализацию.

EnumSingleton.java
package com.journaldev.singleton;
 
public enum EnumSingleton {
 
  INSTANCE;
   
  public static void doSomething(){
    //do something
  }
}

Прочитать про: Enum в Java (на англ.)

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

SerializedSingleton.java
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;
  }
   
}

Проблема с сериализацией для класса синглтон выше, заключается в том, что как только мы воссоздадите объект, он тут же инициализирует экземпляр класса. Рассмотрим на простом примере.
SingletonSerializedTest.java
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

Вот таким образом она разрушает шаблон Singleton. Для предотвращения этой ситуации все, что нам надо сделать, это обеспечить реализацию метода readResolve().

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

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

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

Оригинал статьи:http://www.journaldev.com/1377/java-singleton-design-pattern-best-practices-with-examples

18 комментариев

Sant9Iga
в названии ошибка. «одИночка»
HonyaSaar
fixed
Sant9Iga
а так статья очень хорошая) спасибо большое.
Sant9Iga
habrahabr.ru/post/27108/ тоже не плохая статейка
HonyaSaar
я пока переводил много интересных вещей нашел по синглтону :)
HonyaSaar
habrahabr.ru/post/129494/ -еще интересня с хабра
краткий обзор шаблона на вики
ru.wikipedia.org/wiki/%CE%E4%E8%ED%EE%F7%EA%E0_%28%F8%E0%E1%EB%EE%ED_%EF%F0%EE%E5%EA%F2%E8%F0%EE%E2%E0%ED%E8%FF%29

и тут остальные шаблоны с вики ru.wikipedia.org/wiki/Design_Patterns
nanacano
Совершенно непонятная статья. Чтобы избежать проблем с десериализацией надо переопределить метод readResolve(). Но вопрос как и почему это работает? Я нашел в инете только объяснения на английском, из них нихрена не понятно как это работает.
madalexfiesta
For Serializable and Externalizable classes, the readResolve method allows a class to replace/resolve the object read from the stream before it is returned to the caller. By implementing the readResolve method, a class can directly control the types and instances of its own instances being deserialized.

Для Serializable и Externalizable классов, метод readResolve позволяет классу заменить / разрешить читать объект из потока, прежде чем он возвращается к вызывающему абоненту. При реализации метода readResolve, класс может непосредственно контролировать типы и экземпляры своих собственных экземпляров десериализуемый.
The readResolve method is called when ObjectInputStream has read an object from the stream and is preparing to return it to the caller. ObjectInputStream checks whether the class of the object defines the readResolve method. If the method is defined, the readResolve method is called to allow the object in the stream to designate the object to be returned. The object returned should be of a type that is compatible with all uses. If it is not compatible, a ClassCastException will be thrown when the type mismatch is discovered.
Метод readResolve вызывается, когда ObjectInputStream прочитал объект из потока и готовится к возвращению его к вызывающему абоненту. ObjectInputStream проверяет, определяет ли класс объекта метод readResolve. Если метод определен, readResolve метод вызывается, чтобы позволить объекту в потоке обозначить объект, который будет возвращен. Возвращаемый объект должен быть такого типа, который совместим со всеми видами использования. Если он не совместим, A ClassCastException будет сгенерировано, когда несоответствие типов обнаружено.
Читай по русски, быть может так будет понятней!
ATLUS
  • ATLUS
  • 0
Читаю эту статью с перерывами, потому что появляются новые знаний, сначала узнал что такое сингильтон и не мог понять про нити, сейчас узнал про нити. И осталось узнать про рефлексию…
Dypemap
Спасибо за подготовленный и хорошо изложенный материал!
St1904
Что-то не вижу разницы между Lazy Initialization и Потоко-безопасный Синглтон. Ткните, плз, если она там есть.
ToxyGenn
В потоко-безопасном способе метод getInstance синхронизирован.
DarkCloud
Кто-то может сказать, где и как использовать readResolve ();
Ну реализовал я её в Singleton, а потом??
znorick
readResolve() вызывается внутри readObject(), такая вот особенность java. Подробнее..
isaenkovl
Double-checked locking не работает. Следует упомянуть об этом в статье.
Javaprogrammer
  • Javaprogrammer
  • 0
  • Комментарий отредактирован 2015-04-10 13:49:15 пользователем Javaprogrammer
Не понимаю почему в ранней, статической и ленивой реализации проблемы с многопоточностью? Ведь при каждом обращении отдельного потока к getInstance() он получит значение статической переменной instance , которая всегда будем позвращать один и тот же экземпляр класса, как в такой ситуации могут получится разные экземпляры???
Еще не понимаю это
Проблема с сериализацией для класса синглтон выше, заключается в том, что как только мы воссоздадите объект, он тут же инициализирует экземпляр класса
кто-нибуть можете объяснить поподробней. Спасибо.
talyrus
метод readResolve() в данном случае возвращает переменную экземпляра класса, а не объект
velis
Почему в ранней реализации синглтона написано что экземпляр будет создан в любом случае, даже если мы им не воспользуемся? Ведь в джава классы загружаются только по мере их использования а не все сразу, следовательно и класс синглтона будет загружен только когда бы обратимся к его стат. методу, разве не так?
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.