Вы серьезно не понимаете, что делает volatile
, но, если честно, Интернет и стекопоток, в том числе, просто загрязнены неправильными или неполными ответами по этому поводу. Я также признаю, что я думаю Я хорошо это понимаю, но иногда мне приходится перечитывать некоторые вещи снова.
То, что вы там показали, называется идиомой "двойная проверка блокировки", и это совершенно правильный сценарий использования для создания синглтона. Вопрос в том, действительно ли он вам нужен в вашем случае (другой ответ показал гораздо более простой способ, или вы можете прочитать «enum singleton pattern», если хотите). Немного забавно, что многие люди знают, что volatile
нужен для этой идиомы, но не могут точно сказать , почему это необходимо.
DCL выполняет в основном две вещи - обеспечивает атомарность (несколько потоков не могут одновременно войти в синхронизированный блок) и гарантирует, что после создания все потоки увидят этот созданный экземпляр, называемый видимость . В то же время он гарантирует, что синхронизированный блок будет введен один раз , после этого все потоки не должны будут это делать.
Вы могли бы легко сделать это через:
private Singleton instance;
public Singleton get() {
synchronized (this) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Но теперь каждый Поток, которому нужно это instance
, должен бороться за блокировку и должен войти в этот синхронизированный блок.
Некоторые люди думают, что: «эй, я могу обойти это!» и напишите (при этом введите синхронизированный блок только один раз):
private Singleton instance; // no volatile
public Singleton get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
Все так просто - это сломано . И это не легко объяснить.
он поврежден, потому что есть два независимых чтения из instance
; JMM позволяет переупорядочивать их; таким образом, полностью действительно, что if (instance == null)
не видит ноль; в то время как return instance;
видит и возвращает null
. Да, это нелогично, но вполне допустимо и доказуемо (я могу написать jcstress
тест, чтобы доказать это за 15 минут).
второй пункт немного сложнее. Предположим, в вашем синглтоне есть поле, которое вам нужно установить.
Посмотрите на этот пример:
static class Singleton {
private Object some;
public Object getSome() {
return some;
}
public void setSome(Object some) {
this.some = some;
}
}
И вы пишете такой код, чтобы обеспечить этот синглтон:
private Singleton instance;
public Singleton get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new Singleton();
instance.setSome(new Object());
}
}
}
return instance;
}
Поскольку запись в volatile
(instance = new Singleton();
) происходит за до установки нужного вам поля instance.setSome(new Object());
; какой-то поток, который читает этот экземпляр, может увидеть, что instance
не равен нулю, но при выполнении instance.getSome()
увидит ноль. Правильный способ сделать это (плюс сделать экземпляр volatile
):
public Singleton get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
Singleton copy = new Singleton();
copy.setSome(new Object());
instance = copy;
}
}
}
return instance;
}
Таким образом volatile здесь необходимо для безопасной публикации ; так что опубликованная ссылка «безопасно» видна всем потокам - все ее поля инициализируются. Существуют и другие способы безопасной публикации ссылки, например, final
, заданный в конструкторе и т. Д.
Факт жизни: читает дешевле, чем пишет ; вам не важно, что читает volatile
под капотом, если ваш код верен; так что не беспокойтесь о «чтениях из основной памяти» (или даже лучше не используйте эту фразу, даже не поняв ее частично).