Ответ 1
Причина безопасность потока.
Форма, о которой вы говорили, вы знакомы с потенциалом инициализации синглтона много раз. Более того, даже после того, как он был инициализирован несколько раз, будущие вызовы getInstance()
разными потоками могут возвращать разные экземпляры! Кроме того, один поток может видеть частично инициализированный экземпляр singleton! (предположим, что конструктор подключается к БД и проверяет подлинность: один поток может получить ссылку на синглтон до того, как произойдет аутентификация, даже если это сделано в конструкторе!)
При работе с потоками возникают определенные трудности:
-
Concurrency: они должны потенциально выполняться одновременно;
-
Видимость: изменения в памяти, сделанные одним потоком, могут быть недоступны для других потоков;
-
Переупорядочение: порядок, в котором выполняется код, не может быть предсказан, что может привести к очень странным результатам.
Вы должны изучить эти трудности, чтобы точно понять, почему эти нечетные поведения совершенно законны в JVM, почему они на самом деле хороши и как защитить их.
Статический блок гарантируется JVM, который должен выполняться только один раз (если вы не загружаете и не инициализируете класс с использованием разных 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
имеет значение null, создаст экземпляр Singleton
и сохранит его в поле. Затем он отпустит блокировку и вернет экземпляр.
Затем T2, ожидавший блокировку, сможет его получить и ввести метод. Поскольку он приобрел тот же самый замок, который только что выпустил T1, T2 увидит, что поле instance
содержит тот же самый экземпляр Singleton, созданный T1, и просто вернет его. Более того, инициализация синглтона, которая была выполнена T1, произошла до выпуска блокировки на T1, которая произошла до получения блокировки на T2, поэтому нет способа, чтобы T2 мог видеть частично инициализированный синглтон.
Вышеприведенный код совершенно правильный. Единственная проблема заключается в том, что доступ к singleton будет сериализован. Если это произойдет много, это уменьшит масштабируемость вашего приложения. Поэтому я предпочитаю шаблон 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()
. Это первый, кто когда-либо вводит его, поэтому поле равно null. Затем он входит в синхронизированный блок, затем второй, если. Он также оценивает значение true, поэтому T1 создает новый экземпляр синглтона и сохраняет его в поле. Затем блокировка освобождается, а синглтон возвращается. Для этого потока гарантируется, что Singleton полностью инициализирован.
Теперь T2 входит в метод getInstace()
. Возможно (хотя и не гарантировано), что он увидит, что instance != null
. Затем он пропустит блок if
(и, следовательно, никогда не получит блокировку), и будет напрямую возвращать экземпляр Singleton. Из-за переупорядочения возможно, что T2 не увидит всю инициализацию, выполняемую Singleton в своем конструкторе! Повторяя пример одноканального соединения db, T2 может видеть подключенный, но еще не аутентифицированный синглтон!
Для получения дополнительной информации...
... Я бы рекомендовал блестящую книгу Java Concurrency на практике, а также спецификацию языка Java.