Javenue logo

Javenue

Программирование на Java

Информационные технологии

Паттерн Singleton - Одиночка

[English version of the article can be found here].

Этим постом я открываю цикл статей, посвященных паттернам проектирования. Все написанное мной основывается исключительно на личном опыте.

Паттерны проектирования - это описание некоторых проблем, возникающих во время объектно-ориентированного проектирования, а также способов их решения (как практических, так и теоретических). Иными словами - это примеры правильных подходов к решению типичных задач проектирования.

Одним из самых распространенных паттернов является Singleton (Одиночка). Задача этого шаблона ограничить количество экземпляров некоторого класса в пределах приложения. Зачем это может понадобиться на практике? Об этом читайте чуть ниже. План на сегодня таков:

Простая реализация Singleton

Один из самых простых способов реализовать паттерн Singleton на языке Java выглядит так:

public final class Singleton {
    private static Singleton _instance = null;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (_instance == null)
            _instance = new Singleton();
        return _instance;
    }
}

Теперь приведу некоторые объяснения по поводу реализации шаблона.

Конструктор класса необходимо объявить с модификатором видимости private. Это предотвратит создание экземпляров класса как с помощью класса Singleton, так и с помощью его наследников. В связи с этим к объявлению класса смело можно дописать модификатор final.

Метод getInstance() создаст ровно один экземпляр класса Singleton. Этот метод объявлен как synchronized. Сделано это вот почему. В многопоточных программах при одновременном вызове метода getInstance() из нескольких потоков можно создать несколько экземпляров класса Singleton. А должен остаться только один!

От модификатора synchronized можно избавиться. Для этого _instance нужно проинициализировать:

private static final Singleton _instance = new Singleton(),

а в методе getInstance() убрать конструкцию "if". Тогда инициализация произойдет во время загрузки класса.

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

Синглтон с double-checked locking

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

Самый распространенный способ избавиться от лишней синхронизации - это double-checked locking, который выглядит таким образом:

public final class Singleton {
    private static volatile Singleton _instance = null;

    private Singleton() {}

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

Кстати, до вресии Java SE 1.5 этот код не работал. Если хотите знать почему - читайте здесь.

Синглтон с Instance Holder

А вот еще один заслуживающий внимания вариант реализации шаблона Одиночка с ленивой инициализацией:

public final class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton _instance = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder._instance;
    }
}

Объект будет проинициализирован при первом вызове метода getInstance(). То есть мы перенесли проблему с синхронизацией на уровень загрузчика классов (class loader).

Но что если в приложении несколько Class Loader'ов или вообще у нас распределенная система с несколькими виртуальнми машинами Java? Тогда все сложно. Поговорим об этом в другой раз.

А вообще сейчас модно говорить, что реально крутой вариант паттерна Одиночка выглядит так:

    public enum Singleton {
        INSTANCE;
    }

Вот собственно и все... Ах, да! Зачем нам это нужно.

Практический пример синглтона

Мне приходится чаще всего использовать этот паттерн при работе с конфигурацией. Иногда конфигурацию программы удобно хранить в файле. Допустим, это будет простой текстовый файл "props.txt" со строками типа "ключ=значение". Нам нужно гарантировать, что конфигурация в программе будет в единственном экземпляре. Вторую мы бы и так не создали, но нужно запретить это делать пользователю класса. Итак,

import java.util.*;
import java.io.*;

public class Configuration {
    private static Configuration _instance = null;

    private Properties props = null;

    private Configuration() {
         props = new Properties();
    	try {
	    FileInputStream fis = new FileInputStream(
                    new File("props.txt"));
	    props.load(fis);
    	}
    	catch (Exception e) {
    	    // обработайте ошибку чтения конфигурации
    	}
    }

    public synchronized static Configuration getInstance() {
        if (_instance == null)
            _instance = new Configuration();
        return _instance;
    }

    // получить значение свойства по имени
    public synchronized String getProperty(String key) {
        String value = null;
        if (props.containsKey(key))
            value = (String) props.get(key);
        else {
            // сообщите о том, что свойство не найдено
        }
        return value;
    }
}

Теперь для работы с конфигурацией можно использовать конструкцию вида:

String propValue = Configuration.getInstance().getProperty(propKey).

Если имена свойств в "props.txt" меняться не будут, можно описать их в классе таким образом:

public static final String PROP_KEY = "propKey",

а значения получать так:

String propValue = Configuration.getInstance()
    .getProperty(Configuration.PROP_KEY).

Паттерн Singleton полезен не только при работе с конфигурациями. Его можно использовать также при написании ConnectionPool, Factory и других вещей.

Вот теперь точно все.

Еще по теме: Сколько нужно потоков для обработки пользовательских запросов или крайности при использовании Singleton



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

  Выйти

  * для публикации комментариев нужно  

Oleg:

Не плохо было бы убрать synchronized со всей функции, т.к. не очень optimized :)

c0nst:

2Oleg: если ты имеешь в виду double checked synchronization, то это не работает и заслуживает отдельной статьи.
Или перенести synchronized с метода на блок?

Pavel:

а почему бы не сделать:

private static final INSTANCE = new Singelton();

private Singelton(){…}

public static void getInstance(){
    return INSTANCE;
}
принципиально то же самое что в примере, но немного изящнее и понятнее, и позволяет отказаться от проверки на null.

sdv:

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

private volatile static Singleton instance;

public static Singleton getInstance() {
  if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null)
      instance = new Singleton();
    }
  }
  return instance;
}
Или private static final instance = new Singelton(); и дальше по тексту. Отличия между этими решениями очевидны, но, боюсь, автор его не совсем осознает.

c0nst:

К сожалению мне трудно следить за актуальностью статей, которые я писал в 2005 году. Вот еще статья по теме

.

lativ:

В 2009 можно было бы и предложить вариант через AtomicReference:

public class Singleton {
  private static final AtomicReference SINGL_REF = new AtomicReference();

  private Singleton() { }

  public static final Singleton getInstance() {
    if (SINGL_REF.get() == null) {
      SINGL_REF.compareAndSet(null, new Singleton());
    }
    return SINGL_REF.get();
  }
}

© 2005-2015 Javenue. All Rights Reserved