Можем ли мы избежать использования Volatile в шаблоне Singleton, синхронизировав метод getInstance() в Java?

В большинстве случаев это реализация шаблона проектирования 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()? Если это не сработает, чем будет отличаться выполнение обоих случаев?

Вы можете избежать каждой синхронизации, просто используя private static final Singleton instance = new Singleton(); и public static Singleton getInstance() { return instance; }. Это уже потокобезопасно, лениво инициализировано и настолько эффективно, насколько это возможно. Раздражает, что эта излишне сложная и неэффективная блокировка с двойной проверкой время от времени всплывает. Это бесполезно.

Holger 25.06.2024 11:26

Если вы действительно хотите использовать синглтон, используйте идиому держателя инициализации по требованию .

Radu Sebastian LAZIN 25.06.2024 11:43

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

Holger 25.06.2024 11:48
Этот ответ тоже может помочь.
Holger 25.06.2024 11:51

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

Radu Sebastian LAZIN 25.06.2024 12:03

@RaduSebastianLAZIN перейдите по предоставленным ссылкам, в которых указана спецификация. Класс инициализируется только при определенных действиях, например вызове getInstance(); нет никакой мистической «инициализации загрузчиком классов». Как уже было сказано, для внутренних классов нет разных правил, JVM даже не знает понятия внутренних классов. Вы даже можете создать поле instancepublic и удалить метод, поскольку тот же принцип, по которому вложенный класс инициализируется при первом доступе к его полю static, применим и к классу верхнего уровня.

Holger 25.06.2024 12:18

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

Radu Sebastian LAZIN 25.06.2024 12:23

@RaduSebastianLAZIN это совсем другая тема. В коде вопроса нет другого статического метода, и настоятельно не рекомендуется добавлять другие несвязанные статические элементы. Это принцип единой ответственности. Я не согласен с тем, что код вопроса указывает на намерение добавить другие статические методы, код скорее указывает на то, что автор последовал плохому совету, поскольку двойная проверка блокировки никогда не приносила никакой пользы. Да, если вы хотите добавить другие статические члены, было бы полезно переместить поле синглтона во вложенный класс. Но лучше было бы переместить эти другие статические элементы.

Holger 25.06.2024 12:26

Кстати, synchronized (Singleton.class) аналогично синхронизации статического метода (только избегайте этого каждый раз, когда вызывается метод, то есть, если экземпляр не равен нулю, синхронизация не выполняется/не требуется)

user85421 25.06.2024 13:09

@RaduSebastianLAZIN Кстати, ваш комментарий «память для экземпляра Singleton выделяется, даже если вы вызываете другой статический метод в классе Singleton, а в моем примере это не так» также неверен в отношении вашего примера - вложенный класс в вашей связанной «Инициализации- Идиома держателя по требованию» также будет загружена, если «вы вызываете другой статический метод» в этом вложенном классе (просто классы, которые могут это сделать, более ограничены - но то же самое можно сделать с классом Singleton)

user85421 25.06.2024 13:18

Недостаток синхронизации всего метода заключается в том, что вы создаете узкое место в синхронизации для всех вызовов, даже если синглтон уже давно инициализирован.

Mark Rotteveel 25.06.2024 18:26
Реализация синглтона с помощью Enum (на Java)public enum MySingleton { INSTANCE; private MySingleton() { … } }
Basil Bourque 25.06.2024 19:39

@МаркРоттевел | Насколько я понимаю (пожалуйста, поправьте меня, если я ошибаюсь), если поток A обрабатывает строку создания объекта instance = new Singleton();, то добавление ключевого слова «летучий» означает, что оно предотвращает доступ потока B (чтение/запись) к переменной ( экземпляр) до того, как поток A завершит обработку.

Soumya Sen 26.06.2024 07:11

В этом случае первый if (instance == null) не будет выполняться для потока B, когда поток A все еще создает объект, поэтому не имеет значения, создан объект или нет - мы не сможем прочитать его значение, пока поток A не завершится. его обработка. Таким образом, если мы синхронизируем весь метод, это не должно повлиять на производительность, не так ли?

Soumya Sen 26.06.2024 07:11

@SoumyaSen Нет, Летучий устанавливает предварительное событие между записью значения и последующим чтением. Это гарантирует, что все читатели, даже если они не находятся в синхронизированном блоке, видят последнее обновление поля. Volatile не предотвращает и не блокирует доступ, именно для этого и предназначены взаимное исключение, такое как синхронизированная блокировка. В результате чтение за пределами синхронизированного гарантированно увидит последнюю запись (в противном случае вполне возможно, что другие потоки всегда читают null), в результате потокам вообще не нужно входить в синхронизированный блок, если инициализация была выполнена.

Mark Rotteveel 26.06.2024 09:34

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

Mark Rotteveel 26.06.2024 09:38

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

Mark Rotteveel 26.06.2024 09:42

@user85421 user85421 Я говорил о любом статическом методе в классе Singleton, а не в классе LazyHolder, пожалуйста, посмотрите этот пример: Singleton.

Radu Sebastian LAZIN 26.06.2024 12:16

@RaduSebastianLAZIN, но то, что вы написали («память для одноэлементного экземпляра выделяется, даже если вы вызываете другой статический метод») также верно для «любого статического метода» «в классе LazyHolder» - точно так же, как и в случае с Singleton класса, поэтому утверждение «что касается моего примера, это не так» неверно или также справедливо для класса Singleton.

user85421 26.06.2024 12:24

@user85421 user85421 вы правы, мне следовало указать это более подробно, надеюсь, теперь, когда я привел пример, он стал более понятным. Конечно, если вы реализуете статический метод даже в классе Singleton, который запускает инициализацию (как указал @Holger) класса LazyHolder, память также будет выделена.

Radu Sebastian LAZIN 26.06.2024 13:11
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
20
82
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Мне было интересно, сможем ли мы избежать использования летучих, сделав весь метод 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

Различия между этими двумя:

  • Блокировку с двойной проверкой сложнее реализовать и понять.
  • Блокировка с двойной проверкой более производительна, поскольку не требуется синхронизировать весь метод. В другой версии вызов метода всегда синхронизируется, даже если экземпляр Singleton уже создан.

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

Мне было интересно, сможем ли мы избежать использования изменчивости, синхронизировав весь метод getInstance()? Если это не сработает, чем будет отличаться выполнение обоих случаев?

Конечно, ты можешь это сделать. Но смысл старой идиомы DCL заключался в том, чтобы избежать блокировки... большую часть времени. Если вы объявите getInstance() как synchronized, он будет блокироваться каждый раз, когда вы вызываете метод.


Идиома DCL была изобретена еще тогда, когда блокировка Java была намного дороже. В наши дни бесспорная блокировка (относительно) дешева, поэтому такие хаки, как DCL, менее важны. Более того, существуют другие (лучшие) идиомы для реализации потокобезопасной инициализации. Например:

Хорошие вопросы и ответы о том, почему вышеизложенное работает, см. в разделе Идиома держателя инициализации по требованию — когда загружаются классы?.

Другие вопросы по теме