Синглтон инстанциация - PullRequest
       4

Синглтон инстанциация

18 голосов
/ 19 августа 2011

Ниже показано создание объекта-одиночки.

public class Map_en_US extends mapTree {

    private static Map_en_US m_instance;

    private Map_en_US() {}

    static{
        m_instance = new Map_en_US();
        m_instance.init();
    }

    public static Map_en_US getInstance(){
        return m_instance;
    }

    @Override
    protected void init() {
        //some code;
    }
}

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

public static Map_en_US getInstance(){
    if(m_instance==null)
      m_instance = new Map_en_US();
}

Ответы [ 10 ]

36 голосов
/ 19 августа 2011

Причина заключается в безопасности потоков .

Форма, с которой вы, как вы сказали, знакомы, может инициализировать синглтон большое количество раз.Более того, даже после многократной инициализации будущие вызовы getInstance() различными потоками могут возвращать разные экземпляры!Кроме того, один поток может увидеть частично инициализированный экземпляр singleton!(допустим, конструктор подключается к БД и аутентифицируется; один поток может получить ссылку на одиночный код до того, как произойдет аутентификация, даже если это делается в конструкторе!)

Существуют некоторые трудности, когдаработа с потоками:

  1. Параллелизм : они могут потенциально выполняться одновременно;

  2. Visibility : изменения в памяти, сделанные одним потоком, могут быть невидимы для других потоков;

  3. Изменение порядка : порядок, в котором выполняется код, не может быть предсказан, что может привести к очень странным результатам.

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

JVM гарантирует, что статический блок будет выполняться только один раз (если тольковы загружаетеd инициализировать класс, используя разные ClassLoader s, но детали выходят за рамки этого вопроса, я бы сказал) и только одним потоком, и результаты этого гарантированно будут видимы для каждого другого потока.

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

Мой предпочтительный шаблон: поточно-ориентированный и ленивый

Приведенный выше шаблон будет создавать экземпляр синглтона в первый раз, когдавыполнение видит ссылку на класс Map_en_US (фактически, только ссылка на сам класс загрузит его, но может еще не инициализировать его;для более подробной информации, проверьте ссылку).Может быть, вы этого не хотите.Может быть, вы хотите, чтобы синглтон был инициализирован только при первом вызове Map_en_US.getInstance() (точно так же, как и шаблон, который, как вы сказали, вы знакомы, предположительно).

Если это то, что вы хотите, вы можете использовать следующий шаблон:

public class Singleton {
  private Singleton() { ... }
  private static class SingletonHolder {
    private static final Singleton instance = new Singleton();
  }
  public static Singleton getInstance() {
    return SingletonHolder.instance;
  }
}

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

Еще один шаблон ...

1.synchronized getInstace()

Как уже говорилось в комментариях к этому ответу, есть еще один способ реализовать синглтон в поточно-ориентированном виде, который почти такой же, как (сломанный), с которым вы знакомы:

public class Singleton {
  private static Singleton instance;
  public static synchronized getInstance() {
    if (instance == null)
      instance = new Singleton();
  }
}

Приведенный выше код гарантируется для модели памяти поточно-ориентированной.Спецификация JVM утверждает следующее (более загадочно): пусть L будет блокировкой любого объекта, пусть T1 и T2 будут двумя потоками.Освобождение L T1 происходит до приобретения L T2.

Это означает, что все, что было сделано T1 до снятия блокировки, будет видно для всех остальных потоков.после того, как они получат одну и ту же блокировку.

Итак, предположим, что T1 является первым потоком, который вошел в метод getInstance().Пока он не закончится, ни один другой поток не сможет ввести тот же метод (так как он синхронизирован).Он увидит, что instance равен нулю, создаст экземпляр Singleton и сохранит его в поле.Затем он снимет блокировку и вернет экземпляр.

Тогда T2, который ожидал блокировки, сможет получить ее и ввести метод.Поскольку он получил ту же блокировку, которую только что снял T1, T2 увидит, что поле instance содержит точно такой же экземпляр Singleton, созданного T1, и просто вернет его.Более того, инициализация синглтона, которая была выполнена T1, произошла до снятия блокировки T1, что произошло до получения блокировки T2, поэтому T2 не может увидеть частично инициализированный синглтон.

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

2.Блокировка с двойной проверкой (DCL)

Часто люди боятся стоимости приобретения замка.Я читал, что в настоящее время это не так актуально для большинства приложений.Реальная проблема с получением блокировки заключается в том, что она ухудшает масштабируемость, сериализуя доступ к синхронизированному блоку.

Кто-то изобрел оригинальный способ избежать получения блокировки, и он был назван с двойной проверкойзамок .Проблема в том, что большинство реализаций не работает .То есть большинство реализаций не являются потокобезопасными (т. Е. Столь же поточно-небезопасными, как метод getInstace() в исходном вопросе).

Правильный способ реализации DCL заключается в следующем:

public class Singleton {
  private static volatile Singleton instance;
  public static Singleton getInstance() {
    if (instance == null) {
      synchronized {
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

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

Чтобы понять почему, пусть T1 и T2 - два потока.Давайте сначала предположим, что поле не является изменчивым.

T1 входит в метод getInstace().Это первый вход в него, поэтому поле пустое.Затем он входит в синхронизированный блок, затем второй, если.Он также оценивается как true, поэтому T1 создает новый экземпляр синглтона и сохраняет его в поле.Затем блокировка снимается, а синглтон возвращается.Для этого потока гарантируется, что Singleton полностью инициализирован.

Теперь T2 входит в метод getInstace().Возможно (хотя и не гарантировано), что он увидит, что instance != null.Затем он пропустит блок if (и, следовательно, он никогда не получит блокировку), и будет напрямую возвращать экземпляр Singleton.Из-за переупорядочения , возможно, что T2 не увидит всю инициализацию, выполненную Singleton в его конструкторе!Возвращаясь к примеру синглтона db connection, T2 может увидеть подключенный, но еще не аутентифицированный синглтон!

Для получения дополнительной информации ...

... Я бы порекомендовал блестящую книгу, Java Concurrency на практике , а также Спецификация языка Java .

1 голос
/ 19 августа 2011

Как насчет этого подхода для уничтожения статического блока:

private static Map_en_US s_instance = new Map_en_US() {{init();}};

Он делает то же самое, но гораздо аккуратнее.

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

Также обратите внимание:
m_ - это префикс соглашения об именах для экземпляров (т.е. членов) полей.
s_ - это префикс соглашения об именах для class (то есть статических) полей.
Поэтому я изменил название поля на s_....

1 голос
/ 19 августа 2011

Если вы инициализируете в методе getInstance(), вы можете получить условия гонки, то есть, если 2 потока выполнят проверку if(m_instance == null) одновременно, оба могут увидеть экземпляр равным нулю и, следовательно, оба могут вызвать m_instance = new Map_en_US();

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

Вот хороший обзор.

0 голосов
/ 11 декабря 2013
    // Best way to implement the singleton class in java
    package com.vsspl.test1;

    class STest {

        private static STest ob= null;
        private  STest(){
            System.out.println("private constructor");
        }

        public static STest  create(){

            if(ob==null)
                ob = new STest();
            return ob;
        }

        public  Object  clone(){
            STest   obb = create();
            return obb;
        }
    }

    public class SingletonTest {
        public static void main(String[] args)  {
            STest  ob1 = STest.create();
            STest  ob2 = STest.create();
            STest  ob3 = STest.create();

            System.out.println("obj1  " +  ob1.hashCode());
            System.out.println("obj2  " +  ob2.hashCode());
            System.out.println("obj3  " +  ob3.hashCode());


            STest  ob4 = (STest) ob3.clone();
            STest  ob5 = (STest) ob2.clone();
            System.out.println("obj4  " +  ob4.hashCode());
            System.out.println("obj5  " +  ob5.hashCode());

        }
    }

-------------------------------- OUT PUT -------------------------------------
private constructor
obj1  1169863946
obj2  1169863946
obj3  1169863946
obj4  1169863946
obj5  1169863946
0 голосов
/ 19 августа 2011

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

public class Map_en_US extends mapTree {

    private static
            /* thread safe without final,
               see VM spec 2nd ed 2.17.15 */
            Map_en_US m_instance = createAndInit();

    private Map_en_US() {}

    public static Map_en_US getInstance(){
        return m_instance;
    }

    @Override
    protected void init() {
        //some code;
    }

    private static Map_en_US createAndInit() {
        final Map_en_US tmp = new Map_en_US();
        tmp.init();
        return tmp;
    }
}

обновление исправлено за ВМ спецификации 2.17.5 , подробности в комментариях

0 голосов
/ 19 августа 2011

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

0 голосов
/ 19 августа 2011
  1. При статической реализации будет только одна копия экземпляра на класс независимо от того, сколько объектов создается.
  2. Вторым преимуществом этого способа является метод thread-safe, так как вы ничего не делаете в методе, кроме возврата экземпляра.
0 голосов
/ 19 августа 2011

Интересно, никогда не видел этого раньше.Кажется, во многом стилевые предпочтения.Я полагаю, одно отличие заключается в следующем: статическая инициализация происходит при запуске виртуальной машины, а не при первом запросе экземпляра, что потенциально устраняет проблему с параллельными экземплярами?(Который также может быть обработан с помощью synchronized getInstance() объявления метода)

0 голосов
/ 19 августа 2011

Статический блок выполняется, когда класс впервые загружается JVM. Как сказал Бруно, это помогает с безопасностью потока, потому что нет никакой возможности, что два потока будут бороться за один и тот же вызов getInstance () в первый раз.

0 голосов
/ 19 августа 2011

Зависит от того, насколько ресурсоемким является метод init. Если это, например, делает много работы, возможно, вы хотите, чтобы эта работа выполнялась при запуске приложения, а не при первом вызове Может быть, он загружает карту из Интернета? Я не знаю ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...