Какой смысл делать экземпляр singleton изменчивым при использовании двойной блокировки?

private volatile static Singleton uniqueInstance

Почему в синглтоне при использовании метода двойной блокировки для синхронизации один экземпляр объявляется как volatile? Могу ли я добиться той же функциональности, не объявляя ее изменчивой?


person Phoenix    schedule 24.07.2012    source источник
comment
Связанные сообщения здесь и здесь о том, почему двойная проверка вообще необходима.   -  person RBT    schedule 15.02.2018


Ответы (5)


Без volatile код работает неправильно с несколькими потоками.

Из блокировки с двойной проверкой Википедии:

Начиная с J2SE 5.0, эта проблема устранена. Ключевое слово volatile теперь гарантирует, что несколько потоков правильно обработают одноэлементный экземпляр. Эта новая идиома описана в декларации "Блокировка с двойной проверкой нарушена". :

// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }

    // other functions and members...
}

В общем, вам следует по возможности избегать блокировки с двойной проверкой, так как это трудно сделать правильно, а если вы ошиблись, может быть трудно найти ошибку. Вместо этого попробуйте этот более простой подход:

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

// Correct lazy initialization in Java 
@ThreadSafe
class Foo {
    private static class HelperHolder {
       public static Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}
person Mark Byers    schedule 24.07.2012
comment
Блокировка с двойной проверкой может быть необходима в определенных ситуациях, когда у вас есть синглтон, который может измениться во время выполнения, но может существовать в вашей программе только один раз. то есть сеанс входа в систему, который вы хотите использовать для доступа к сайту, но вам нужно время от времени повторно подключаться и получать новый. Однако, если значение не изменяется во время выполнения, вам следует избегать его. - person Xtroce; 30.03.2016
comment
Как вы будете передавать аргументы конструктору в случае идиомы держателя? - person Aamir Rizwan; 02.02.2018
comment
Разве блок synchronized не гарантирует, что некэшированные значения полей будут извлечены, а это означает, что часть volatile больше не нужна? Требуется ли это и сегодня? - person android developer; 27.01.2021

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

Рассмотрим такую ​​ситуацию: поток A обнаруживает, что uniqueInstance == null, блокируется, подтверждает, что он все еще null, и вызывает конструктор синглтона. Конструктор выполняет запись в элемент XYZ внутри Singleton и возвращает значение. Теперь поток A записывает ссылку на вновь созданный синглтон в uniqueInstance и готовится снять блокировку.

Как только поток A готовится снять блокировку, появляется поток B и обнаруживает, что uniqueInstance не является null. Поток B обращается к uniqueInstance.XYZ, думая, что он был инициализирован, но поскольку ЦП переупорядочил записи, данные, которые поток A записал в XYZ, не стали видимыми для потока B. Поэтому поток B видит внутри XYZ неверное значение, т.е. неправильный.

Когда вы отмечаете uniqueInstance volatile, вставляется барьер памяти. Все операции записи, инициированные до uniqueInstance, будут завершены до того, как будет изменен uniqueInstance, что предотвратит описанную выше ситуацию переупорядочивания.

person Sergey Kalinichenko    schedule 24.07.2012
comment
Спасибо за ответ - person Abhijit Gaikwad; 07.11.2013
comment
Чтобы быть конкретным, две переупорядоченные записи: 1) A назначает адрес памяти для uniqueInstance и 2) XYZ получает что-то значимое. - person lcn; 15.06.2015
comment
@dasblinkenlight: вы сказали это, потому что ЦП переупорядочил записи..... Пожалуйста, объясните, что означает переупорядочение записей? - person tharindu_DG; 24.07.2015
comment
Как только поток A готовится снять блокировку, действительно ли это имеет значение? - person gstackoverflow; 18.06.2019
comment
@dasblinkenlight В чем смысл предотвращения переупорядочения операций записи в память? - person Neelabh Singh; 15.02.2020
comment
@NeelabhSingh Буквально это означает то, что он говорит: если ваш код говорит, что хочет записать в местоположение A перед местоположением B, наличие volatile гарантирует, что A действительно записывается перед B. Без volatile ЦП может свободно записывать B перед A , если это не оказывает заметного влияния на логику вашего кода. - person Sergey Kalinichenko; 15.02.2020
comment
что, если экземпляр имеет две переменные, XYZ инициализируется, но DEF нет, а XYZ инициализируется, и до того, как DEF сможет запуститься, приходит другой поток и видит ненулевое значение и пытается получить доступ к DFE, поскольку предыдущий поток не пытался начать запись DEF, как будет изменчиво помощь? - person user124; 28.03.2021
comment
@ user124 другой поток не может видеть экземпляр, пока не будет сохранен одноэлементный указатель. - person Sergey Kalinichenko; 28.03.2021
comment
но как только вы создаете память. instance= новый экземпляр, вы его инициализировали, теперь t1 делает это, а также делает intance.XYZ=123, а следующий оператор - instance.DEF, немного раньше, чем он делает статус DEF, он был вытеснен после завершения instance.XYZ, теперь у процессора нет ожидающих операций записи, так как поток пытался запустить instance.DEF, что, если поток 2 придет и прочитает его? - person user124; 29.03.2021
comment
@user124 user124 Вы не назначаете `экземпляр= новый экземпляр(), you assign a local tmp = новый экземпляр(), then check again, and finally do instance = tmp`. См. код Марка в принятом ответе. - person Sergey Kalinichenko; 29.03.2021
comment
ах получил это. Слишком много сложности :(. Можем ли мы просто иметь статическую переменную как , private static final Foo INSTANCE = new Foo();, и тогда наш getInstance может просто вернуть это вместо volatile и всех потоков, а также это потокобезопасно, я видел некоторые сообщения, предлагающие этот путь. - person user124; 30.03.2021
comment
Не поможет ли, если вручную вызвать Thread.MemoryBarrier(); перед назначением переменной? - person Bogdan Mart; 09.04.2021
comment
О, это Java, а не С#. Виноват - person Bogdan Mart; 09.04.2021

Чтобы избежать использования двойной блокировки или нестабильности, я использую следующее

enum Singleton {
     INSTANCE;
}

Создание экземпляра простое, ленивая загрузка и потокобезопасность.

person Peter Lawrey    schedule 25.07.2012

Запись в изменчивое поле произойдет до любой операции чтения. Ниже приведен пример кода для лучшего понимания:

private static volatile ResourceService resourceInstance;
//lazy Initialiaztion
public static ResourceService getInstance () {
    if (resourceInstance == null) { // first check
        synchronized(ResourceService.class) {
            if (resourceInstance == null) { // double check
                // creating instance of ResourceService for only one time
                resourceInstance = new ResourceService ();                    
            }
        }
    }
    return resourceInstance;
}

Эта ссылка может помочь вам лучше http://javarevisited.blogspot.com/2011/06/volatile-keyword-java-example-tutorial.html

person RajeeV VenkaT    schedule 08.06.2016

Вы можете использовать следующий код:

private static Singleton uniqueInstance;

public static synchronized Singleton getInstance(){
    if(uniqueInstance == null){
        uniqueInstance = new Singleton();
    }
    return uniqueInstance
}
person user8022958    schedule 17.05.2017