Java Double Locking - може ли някой да обясни по-просто защо интуицията не работи?

Намерих следния код тук: http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

Опитвам се да разбера защо има определени случаи, в които това не работи. Прочетох обяснението на "фините" проблеми и че използването на volatile ще реши проблема, но съм малко объркан.

// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }

    // other functions and members...
}

По принцип прав ли съм да предположа, че това ще се провали поради факта, че проверката helper == null в блока synchronized има шанс да се провали, защото може да бъде "частично" конструирана в този момент? Java не връща ли нула, ако даден обект е частично конструиран? Това ли е проблемът?

Както и да е, знам, че не е добра практика да се прави заключване с двойна проверка, но просто бях любопитен на теория защо горният код се проваля и защо volatile (плюс добавянето на присвояване на локална променлива) коригира това? Ето някакъв код, който взех отнякъде.

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
    FieldType result = field;
    if (result == null) { // First check (no locking)
        synchronized(this) {
            result = field;
            if (result == null) // Second check (with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}

Знам, че вече има хиляди публикации за това, но обясненията изглежда споменават промени в модела на паметта след 1.5 и не разбирам какво общо има това също :-(.

Благодаря предварително!


person K2xL    schedule 16.09.2012    source източник


Отговори (2)


прав ли съм да предположа, че това ще се провали поради факта, че помощната == нулева проверка в синхронизирания блок има шанс да се провали, защото може да бъде "частично" конструирана в този момент?

Да, прав си. Това е обяснено в Записи извън ред. helper = new Helper() се състои от 3 стъпки: разпределение на паметта, извикване на конструктора и присвояване. JIT компилаторът е свободен да пренарежда инструкции и да извършва присвояване след заделяне на памет (което връща препратка към новия обект), но преди извикването на конструктора. Използването на volatile предотвратява пренареждането.

person Alexei Kaigorodov    schedule 16.09.2012
comment
въпрос и това може да е глупаво, но защо java не изисква всичките три стъпки да завършат, преди == null да се изчисли на false? - person K2xL; 16.09.2012
comment
защото сравняването с null е в различна нишка. Ако беше в същата нишка, тогава никога няма да видите частично конструиран обект. - person Alexei Kaigorodov; 16.09.2012
comment
@K2xL Моделът на паметта на Java казва, че пренареждането на присвояването на местоположението в паметта и инициализацията са достъпни като оптимизация на производителността на HotSpot, освен ако не му кажете друго. Използвате безопасно публикуване (нестабилни, крайни полета, каквото и да е и т.н.), за да го кажете друго. Тази способност за оптимизиране е важна, ако беше безопасна за нишки по подразбиране, също щеше да бъде много по-бавна. - person Jed Wesley-Smith; 17.09.2012
comment
@JedWesley-Smith интересно, чудя се при какви обстоятелства би било по-бързо да се присвои местоположението на паметта по-рано... - person K2xL; 17.09.2012
comment
В допълнение към пренареждането на инструкциите, другият класически проблем с DCL (което се случва в Java, както и в други езици) е в многопроцесорни системи, кеширането може да доведе до промените, извършени от един процесор, да се възприемат като неправилни на друг. Вижте cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html за добро пълно описание на проблемите с DCL в Java. - person Jason C; 06.08.2013

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

Надявам се това да помогне! В противен случай мога да препоръчам да вземете наистина чаша силно кафе, много тиха стая и след това да прочетете Модел на паметта на Java, който обяснява как работи и взаимодействието между нишките. Мисля, че ще се изненадате колко малко ситуации е необходима нишка, за да комуникира записите си в (споделена) памет на други нишки и пренареждането на четения и записи, които JVM може да извърши!

Вълнуващо четиво!

person Nicholas    schedule 16.09.2012
comment
Не е съвсем правилно за volatile: 'synchronized' също се изхвърля в основната памет. Никога не можете да използвате volatile, само „синхронизирани“ и да имате правилни програми. Също така може да се види частично изграден обект, вижте препратка в моя отговор. - person Alexei Kaigorodov; 16.09.2012