В большинстве случаев это реализация шаблона проектирования Singleton.
Он использует ключевое слово volatile
, чтобы предотвратить проблему частичного создания объекта (устанавливает отношение «случается до того, как произойдет чтение» и запись в instance
).
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Мне было интересно, сможем ли мы избежать использования летучих, синхронизировав весь метод getInstance()
? Если это не сработает, чем будет отличаться выполнение обоих случаев?
Если вы действительно хотите использовать синглтон, используйте идиому держателя инициализации по требованию .
@RaduSebastianLAZIN даже этот шаблон излишне сложен. Инициализация внешнего класса такая же ленивая, как и инициализация внутреннего класса (для JVM нет разницы между внутренними классами и классами верхнего уровня). Просто сделайте, как описано в моем первом комментарии , и инициализация будет выполнена при первом вызове getInstance()
. В связанной статье путаются «загрузка» и «инициализация». Смотрите этот ответ.
@Холгер, вы ошибаетесь, статический внутренний класс инициализируется (также согласно вашей ссылке) только при вызове метода getInstance
, следовательно, память для синглтона выделяется только в тот момент, когда в вашем примере память выделяется, когда Singleton
класс загружается (и инициализируется) загрузчиком классов.
@RaduSebastianLAZIN перейдите по предоставленным ссылкам, в которых указана спецификация. Класс инициализируется только при определенных действиях, например вызове getInstance()
; нет никакой мистической «инициализации загрузчиком классов». Как уже было сказано, для внутренних классов нет разных правил, JVM даже не знает понятия внутренних классов. Вы даже можете создать поле instance
public
и удалить метод, поскольку тот же принцип, по которому вложенный класс инициализируется при первом доступе к его полю static
, применим и к классу верхнего уровня.
@Holder, вы правы, но (как я уже упоминал) в вашем примере память для одноэлементного экземпляра выделяется, даже если вы вызываете другой статический метод в классе Singleton
, а в моем примере это не так. И вопрос показывает, что это предполагаемое поведение, чтобы память выделялась только при вызове метода getInstance
.
@RaduSebastianLAZIN это совсем другая тема. В коде вопроса нет другого статического метода, и настоятельно не рекомендуется добавлять другие несвязанные статические элементы. Это принцип единой ответственности. Я не согласен с тем, что код вопроса указывает на намерение добавить другие статические методы, код скорее указывает на то, что автор последовал плохому совету, поскольку двойная проверка блокировки никогда не приносила никакой пользы. Да, если вы хотите добавить другие статические члены, было бы полезно переместить поле синглтона во вложенный класс. Но лучше было бы переместить эти другие статические элементы.
Кстати, synchronized (Singleton.class)
аналогично синхронизации статического метода (только избегайте этого каждый раз, когда вызывается метод, то есть, если экземпляр не равен нулю, синхронизация не выполняется/не требуется)
@RaduSebastianLAZIN Кстати, ваш комментарий «память для экземпляра Singleton выделяется, даже если вы вызываете другой статический метод в классе Singleton, а в моем примере это не так» также неверен в отношении вашего примера - вложенный класс в вашей связанной «Инициализации- Идиома держателя по требованию» также будет загружена, если «вы вызываете другой статический метод» в этом вложенном классе (просто классы, которые могут это сделать, более ограничены - но то же самое можно сделать с классом Singleton
)
Недостаток синхронизации всего метода заключается в том, что вы создаете узкое место в синхронизации для всех вызовов, даже если синглтон уже давно инициализирован.
public enum MySingleton { INSTANCE; private MySingleton() { … } }
@МаркРоттевел | Насколько я понимаю (пожалуйста, поправьте меня, если я ошибаюсь), если поток A обрабатывает строку создания объекта instance = new Singleton();
, то добавление ключевого слова «летучий» означает, что оно предотвращает доступ потока B (чтение/запись) к переменной ( экземпляр) до того, как поток A завершит обработку.
В этом случае первый if (instance == null)
не будет выполняться для потока B, когда поток A все еще создает объект, поэтому не имеет значения, создан объект или нет - мы не сможем прочитать его значение, пока поток A не завершится. его обработка. Таким образом, если мы синхронизируем весь метод, это не должно повлиять на производительность, не так ли?
@SoumyaSen Нет, Летучий устанавливает предварительное событие между записью значения и последующим чтением. Это гарантирует, что все читатели, даже если они не находятся в синхронизированном блоке, видят последнее обновление поля. Volatile не предотвращает и не блокирует доступ, именно для этого и предназначены взаимное исключение, такое как синхронизированная блокировка. В результате чтение за пределами синхронизированного гарантированно увидит последнюю запись (в противном случае вполне возможно, что другие потоки всегда читают null
), в результате потокам вообще не нужно входить в синхронизированный блок, если инициализация была выполнена.
Отсутствие необходимости входа в синхронизированный блок позволяет избежать узкого места параллелизма, поскольку только один поток одновременно может войти в любой синхронизированный блок на конкретном объекте (мониторе). Итак, представьте, что 100 потоков хотят получить синглтон одновременно. Если вы синхронизируете весь метод, то они будут получать синглтон один за другим (последовательно), если вы используете обычный шаблон блокировки с двойной проверкой (а синглтон уже установлено), то все эти 100 потоков получают значение одновременно (параллельно), поскольку им никогда не придется входить в синхронизированный блок.
Если синглтон еще не инициализирован, несколько потоков пройдут первый, если один поток входит в синхронизированный блок для инициализации синглтона, а другие потоки ждут входа в синхронизированный блок. Как только первый поток выходит из синхронизированного блока, следующий ожидающий поток входит в синхронизированный блок, но видит, что он уже инициализирован, и выходит из синхронизированного блока и т. д. Между тем, новые потоки, запрашивающие синглтон, никогда не будут ждать в синхронизированном блоке. блокировать, но вместо этого получить значение немедленно.
@user85421 user85421 Я говорил о любом статическом методе в классе Singleton
, а не в классе LazyHolder
, пожалуйста, посмотрите этот пример: Singleton.
@RaduSebastianLAZIN, но то, что вы написали («память для одноэлементного экземпляра выделяется, даже если вы вызываете другой статический метод») также верно для «любого статического метода» «в классе LazyHolder
» - точно так же, как и в случае с Singleton
класса, поэтому утверждение «что касается моего примера, это не так» неверно или также справедливо для класса Singleton
.
@user85421 user85421 вы правы, мне следовало указать это более подробно, надеюсь, теперь, когда я привел пример, он стал более понятным. Конечно, если вы реализуете статический метод даже в классе Singleton
, который запускает инициализацию (как указал @Holger) класса LazyHolder
, память также будет выделена.
Мне было интересно, сможем ли мы избежать использования летучих, сделав весь метод getInstance() синхронизирован? Если не получится, то как будет ли исполнение обоих дел отличаться?
Это возможно и упрощает код, но имеет последствия.
Пример ленивой загрузки синглтона с использованием блокировки с двойной проверкой: https://github.com/iluwatar/java-design-patterns/blob/master/singleton/src/main/java/com/iluwatar/singleton/ThreadSafeLazyLoadedIvoryTower.java
Пример синглтона с отложенной загрузкой с использованием синхронизированного метода: https://github.com/iluwatar/java-design-patterns/blob/master/singleton/src/main/java/com/iluwatar/singleton/ThreadSafeLazyLoadedIvoryTower.java
Различия между этими двумя:
Таким образом, хотя блокировка с двойной проверкой является более сложной, она обеспечивает значительные преимущества в производительности в многопоточных средах, что делает ее предпочтительной в сценариях, где производительность имеет решающее значение.
Мне было интересно, сможем ли мы избежать использования изменчивости, синхронизировав весь метод getInstance()? Если это не сработает, чем будет отличаться выполнение обоих случаев?
Конечно, ты можешь это сделать. Но смысл старой идиомы DCL заключался в том, чтобы избежать блокировки... большую часть времени. Если вы объявите getInstance()
как synchronized
, он будет блокироваться каждый раз, когда вы вызываете метод.
Идиома DCL была изобретена еще тогда, когда блокировка Java была намного дороже. В наши дни бесспорная блокировка (относительно) дешева, поэтому такие хаки, как DCL, менее важны. Более того, существуют другие (лучшие) идиомы для реализации потокобезопасной инициализации. Например:
Хорошие вопросы и ответы о том, почему вышеизложенное работает, см. в разделе Идиома держателя инициализации по требованию — когда загружаются классы?.
Вы можете избежать каждой синхронизации, просто используя
private static final Singleton instance = new Singleton();
иpublic static Singleton getInstance() { return instance; }
. Это уже потокобезопасно, лениво инициализировано и настолько эффективно, насколько это возможно. Раздражает, что эта излишне сложная и неэффективная блокировка с двойной проверкой время от времени всплывает. Это бесполезно.