Използване на volatile в Singleton (решение на Bill Pughs)

По-долу е даден клас на Java, използващ сингълтън решение на Bill Pugh.

public class Singleton {

    int nonVolatileVariable;

    private static class SingletonHelper {
        private static Singleton INSTANCE = new Singleton();
    }

    private Singleton() { }

    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    }

    public int getNonVolatileVariable() {
        return nonVolatileVariable;
    }

    public void setNonVolatileVariable(int nonVolatileVariable) {
        this.nonVolatileVariable= nonVolatileVariable;
    }
}

На много места съм чел, че този подход е безопасен за нишки. Така че, ако го разбирам правилно, тогава единичният екземпляр се създава само веднъж и всички нишки, които имат достъп до метода getInstance, ще получат един и същ екземпляр от класа Singleton. Въпреки това се чудех дали нишките могат локално да кешират получения единичен обект. Могат ли? Ако отговорът е „да“, това не би означавало, че всяка нишка може да промени полето на екземпляр nonVolatileVariable на различни стойности, което може да създаде проблеми.

Знам, че има други методи за създаване на сингълтън, като например enum singleton, но съм особено заинтересован от този метод за създаване на сингълтон.

Въпросът ми е, че има ли нужда да се използва променливата ключова дума като int volatile nonVolatileVariable;, за да се уверите, че сингълтонът, използващ този подход, е наистина безопасен за нишки? Или вече наистина е безопасно за нишки? Ако да как?


person Deepak Shajan    schedule 17.03.2018    source източник
comment
Без volatile може да се случи някоя нишка да не види промените в nonVolatileVariable, направени от друга нишка.   -  person lexicore    schedule 17.03.2018


Отговори (2)


Въпросът ми е, че има ли нужда да се използва ключовата дума volatile като int volatile nonVolatileVariable; за да се уверите, че сингълтонът, използващ този подход, е наистина безопасен за нишки? Или вече наистина е безопасно за нишки? Ако да как?

Единичният модел гарантира, че е създаден единичен екземпляр на класа. Това не гарантира, че полетата и методите са безопасни за нишки, а volatile също не го гарантира.

Въпреки това се чудех дали нишките могат локално да кешират получения единичен обект?

Според модела на паметта в Java, да, могат.

ако да, тогава не, това би означавало, че всяка нишка може да промени полето на екземпляр nonVolatileVariable на различни стойности, което може да създаде проблеми.

Наистина, но все пак ще имате проблем с последователността с променлива volatile, защото volatile обработва въпроса за видимостта на паметта, но не обработва синхронизацията между нишките.

Опитайте следния код, където множество нишки увеличават volatile int 100 пъти.
Ще видите, че не можете да получите 100 всеки път като резултат.

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Singleton {

    volatile int  volatileInt;

    private static class SingletonHelper {
        private static Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    }

    public int getVolatileInt() {
        return volatileInt;
    }

    public void setVolatileInt(int volatileInt ) {
        this.volatileInt = volatileInt ;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        List<Callable<Void>> callables = IntStream.range(0, 100)
                                                  .mapToObj(i -> {
                                                      Callable<Void> callable = () -> {
                                                          Singleton.getInstance().setVolatileInt(Singleton.getInstance().getVolatileInt()+1);
                                                          return null;
                                                      };
                                                      return callable;
                                                  })
                                                  .collect(Collectors.toList());

        executorService.invokeAll(callables);

        System.out.println(Singleton.getInstance().getVolatileInt());
    }

}

За да сте сигурни, че всяка нишка взема под внимание други извиквания, трябва да използвате външна синхронизация и в този случай не е необходимо да правите променливата volatile.

Например :

synchronized (Singleton.getInstance()) {
  Singleton.getInstance()
           .setVolatileInt(Singleton.getInstance().getVolatileInt() + 1);
}

И в този случай volatile вече не е необходим.

person davidxxx    schedule 17.03.2018
comment
интересно Значи ли това, че по никакъв начин не можем да гарантираме, че моят клас Singleton може да бъде направен строго безопасен за нишки само чрез добавяне на volatile или synchronized в рамките? - person Deepak Shajan; 18.03.2018
comment
Външната синхронизация, която споменах в края на отговора си, е синхронизация на java нишка. Добавих пример. - person davidxxx; 18.03.2018
comment
Това, което разбрах от примера е, че трябва да използваме външна синхронизация на местата, където извикваме метода getInstance() или сетер. Но това е все едно да наложим допълнителна отговорност на повикващия. Можем ли по някакъв начин да направим нашия единичен екземпляр нишката безопасна, така че методът getInstance() просто да връща нишково безопасен обект?? - person Deepak Shajan; 18.03.2018
comment
Методите на обект са безопасни за нишки в ограничението на методите, извиквани едновременно. Например Vector е безопасен за нишка, но изпълнява съставена обработка: извикването на методи get() и add() без синхронизация не гарантира, че съставената обработка от нишка е безопасна, защото имате състояние на състезание. Разбира се, ако извикате getVolatileInt() и setVolatileInt() независимо от резултата един от друг, вече няма да имате състояние на състезание и не е необходима външна синхронизация. - person davidxxx; 18.03.2018

Специфичната гаранция на този тип сингълтън работи основно по следния начин:

  1. Всеки клас има уникално заключване за инициализация на класа.
  2. Всяко действие, което може да причини инициализация на класа (като достъп до статичен метод), е необходимо първо да се получи това заключване, да се провери дали класът трябва да бъде инициализиран и, ако е така, да се инициализира.
  3. Ако JVM може да определи, че класът вече е инициализиран и текущата нишка може да види ефекта от това, тогава може да пропусне стъпка 2, включително пропускане на придобиването на заключването.

(Това е документирано в § 12.4.2.)

С други думи, това, което е гарантирано тук, е, че всички нишки трябва поне да видят ефектите от присвояването в private static Singleton INSTANCE = new Singleton(); и всичко останало, което е извършено по време на статичната инициализация на класа SingletonHelper.

Вашият анализ, че едновременните четения и записи на не-volatile променливата могат да бъдат непоследователни между нишките, е правилен, въпреки че езиковата спецификация не е написана от гледна точка на кеширане. Начинът, по който е написана езиковата спецификация, е, че четенето и записът могат да изглеждат неправилно. Да предположим например следната последователност от събития, изброени хронологично:

nonVolatileVariable is 0
ThreadA sets nonVolatileVariable to 1
ThreadB reads nonVolatileVariable (what value should it see?)

Езиковата спецификация позволява на ThreadB да види стойността 0, когато прочете nonVolatileVariable, което е все едно събитията са се случили в следния ред:

nonVolatileVariable is 0
ThreadB reads nonVolatileVariable (and sees 0)
ThreadA sets nonVolatileVariable to 1

На практика това се дължи на кеширане, но езиковата спецификация не казва какво може или не може да се кешира (освен тук и тук, като кратко споменаване), той само определя реда на събитията.


Една допълнителна забележка относно безопасността на нишките: някои действия винаги се считат за атомарни, като четене и запис на препратки към обекти (§17.7), така че има някои случаи, в които използването на не-volatile променлива може да се счита за нишка- безопасно, но зависи какво конкретно правите с него. Все още може да има несъответствие в паметта, но едновременните четения и записи не могат по някакъв начин да преплитат, така че не можете да завършите с напр. невалидна стойност на указател по някакъв начин. Следователно понякога е безопасно да се използват не-volatile променливи за напр. лениво инициализирани полета ако няма значение, че процедурата по инициализация може да се случи повече от веднъж. Знам за поне едно място в JDK, където това се използва, в java.lang.reflect.Field (вижте също този коментар във файла), но това не е норма.

person Radiodef    schedule 17.03.2018