Zatraceně líná inicializace

Dneska jsem strávil pěkných pár hodin hledáním záhadné chyby. Nedávno jsme v práci implementovali webové služby nad stávající aplikací. Protože jsem se kdysi infikoval testováním, tak jsem si v SoapUI vyrobil takovou pěknou sadu automatických testů. Nejsou to unit testy. Unit testy máme pokrytou vrstvu zařizující webové služby. Pomocí SoapUI děláme funkční testy, které otestují všechno odshora dolů. Testují webové služby, stávající kód aplikace, který není testy pokryt skoro vůbec, a dokonce i interakci s několika systémy na pozadí. Tyto funkční testy obvykle procházejí, pokud tedy počasí přeje, v databázi jsou ta správná data a lety do Amsterdamu nejsou zrovna moc v módě.
Včera odpoledne jsem si ale všiml jedné zajímavé věci. Když nastartuji server a pustím všechny testy najednou, většina z nich selže. Když jsem se podíval do logu, tak jsem uviděl pěknou řadu NullPointerException vyhozených z knihovny Castor. Tu používáme pro serializaci do XML při komunikaci s jedním systémem pod námi. Zajímavé bylo, že když jsem ty testy spustil kdykoliv později, všechny prošly (až na výjimky uvedené dříve). Chvíli jsem si pohrával s myšlenku chybu ignorovat. S Castorem nikdy problémy nebyly, používají ho spokojeně několik let. Třeba je to jen nějaká taková podivnost s kterou se přeci nemusím zabývat. No ale nakonec moje poctivé já zvítězilo a vrhl jsem se do zkoumání. Přeci jenom ve mně trochu hlodaly pochyby, že by to mohlo být nějak způsobené mojí chybou. Ano, dokonce i já o své genialitě někdy pochybuji.
Nejdřív jsem si ověřil, že se to děje pouze hned po nastartování aplikace. A to jenom když spustím několik dotazů najednou. Klasické indicie na problém se synchronizací vláken. Zkusil jsem osvědčené řešení. Zeptal jsem se pana Googla. Světe div se, ani on mi neuměl poradit. To by naznačovalo, že je chyba na naší straně. Castor býval docela populární, kdyby v něm byla chyba, někdo by na ni přeci narazil.
Ale naše použití Castoru bylo naprosto v pořádku. Je tak jednoduché, že na něm není co zkazit

Marshaller.marshal(objectToBeMarshalled, writer);

Takže jsem se ocitl zase na začátku. Dobré bylo, že k chybě docházelo vždy na tom samém řádku. Takže se dal použít debugger. Po pár hodinách strávením restartováním serveru, debuggováním a přemýšlením co dělám špatně jsem přišel na to, že se vše točí kolem třídy DurationDescriptor. Když jsem uviděl její konstruktor, bylo mi jasné že jsem narazil na viníka. Posuďte sami

    private static XMLFieldDescriptorImpl _contentDescriptor = null;


    //----------------/
    //- Constructors -/
    //----------------/

    public DurationDescriptor() {
        super(_xmlName, Duration.class);
        if (_contentDescriptor == null) {
            _contentDescriptor = new XMLFieldDescriptorImpl(String.class,
                "content", "content", NodeType.Text);
            //-- setHandler
            _contentDescriptor.setHandler(new DurationFieldHandler());
        }

    } 
    ...
    _contentDescriptor.getHandler().getValue();

Zkušení harcovníci už určitě vidí kde je problém a mlátí hlavou o stůl. Ostatním se to pokusím vysvětlit.
Představte si že mám dvě vlákna, pojmenujme si je Franta a Pepík. Oba bohužel trpí takovou ošklivou nemocí, kvůli které usínají v ten nejnevhodnější okamžik. Začněme u Pepíka. Ten skočí do konstruktoru a jukne se na statický _contentDescriptor. Ten je prázný takže se Pepík rozhodne ho naplnit. Jenže než to stihne, tak usne někde na cestě mezi řádkami 9 a 10. To je příležitost pro Frantu. Ten se vrhne do konstruktoru, mrkne na _contentDescriptor, ten je pořád ještě prázdný a tak se rozhodne ho naplnit. Obcházení spícího Pepíka ho ale unaví natolik, že z toho taky usne. Jak sebou žuchne probudí Pepíka, který je plný elánu. Vyrobí XMLFieldDescriptorImpl, nastaví Handler a vyrazí do světa. Bohužel usne těsně před tím než stihne Handler použít. Mezitím se vzbudí Franta. Ten si tolik neodpočinul, jenom si vzpomene, že chtěl vyrobit XMLFieldDescriptorImpl. Tak to udělá ale ukládání reference do proměnné _contentDescriptor ho unaví ho to natolik, že zas hned usne. Mezitím se vzbudí energičtější Pepík, který zrovna potřebuje použít Handler. Tak zavolá _contentDescriptor.getHandler(). A co dostane milé děti? null přeci. Zazvonil zvonec a já mám plný log NullPointerException.
Na obhajobu autorů Castoru musím říci, že chybu opravili v roce 2006 commitem číslo 6421. Bohužel ji opravili potichu a nikomu o ní neřekli, takže mi pan Google neuměl poradit. Opravená verze vypadá následovně.

    private static final XMLFieldDescriptorImpl _contentDescriptor;

    static {
        _contentDescriptor = new XMLFieldDescriptorImpl(String.class, "content",
                                                        "content", NodeType.Text);
      _contentDescriptor.setHandler(new DurationDescriptor().new DurationFieldHandler());
    }

Je to mnohem jednodušší a bezpečnější. JVM mi zaručí, že se statický inicializátor zavolá nejvýše jednou.
Jaké z toho plyne poučení? Jedno je evidentní. Používejte nejnovější verze knihoven. (Pokud tedy můžete, my máme Castor upravený od subdodavatele, nikdo neví jaký je rozdíl oproti standardní verzi takže se nikdo neodváží upgradovat na novější verzi).
Druhé poučení je složitější. Dejte si pozor na línou inicializaci. Není to tak jednoduché jak to vypadá. Pokud má někdo dojem, že by to vyřešilo vhodně umístěné klíčové slovo synchronize, přečtěte si tento článek. Není to vůbec jednoduché.
Používání líné inicializace je jistý druh optimalizace. Většinou se k ní uchylujeme, když máme dojem, že můžeme ušetřit pár drahocenných instrukcí. Většinou to ale nestojí za to. Pokud si opravdu myslíte, že ji potřebujete, udělejte to jak radí šílený Bob Lee. No a já se s vámi rozloučím citací zlatých pravidel optimalizace:

Pravidlo 1: Neoptimalizujte
Pravidlo 2: (jen pro experty) Zatím neoptimalizujte

6 thoughts on “Zatraceně líná inicializace

  1. ufak

    Krome pekneho slohu zabavne a pochopitelne napsano. Diky za osvezeni.
    Lina inicializace se ma pouzit tam, kde je mozne, ze se objekt mozna nebude vytvaret, nebo se nemusi vytvaret hned – napr. pri loadu aplikace. Taky uz jsem si natlouk, ze me synchronize nezachranila.

  2. Ladislav Thon

    Pěkně napsané. Sice se to absolutně netýká tématu článku, ale podle mne to nejdůležitější z celého článku se skrývá v závorce 🙂

    > Pokud tedy můžete, my máme Castor upravený od subdodavatele, nikdo neví jaký je rozdíl oproti standardní verzi takže se nikdo neodváží upgradovat na novější verzi

    To je ten největší průšvih, jaký může nastat. My třeba máme upravený Jetspeed (už hodně prehistorickou verzi). Poslední verze Velocity, proti které lze ta mrcha přeložit, je 1.4 — a zrovna před pár týdny jsme narazili na bug http://issues.apache.org/jira/browse/VELOCITY-109, který je opravený až ve verzi 1.5. A teď babo raď 🙂

  3. Matty

    Dikes za paradni clanek (pohadka pro dospele :-)) o necem, co je fajn mit na zreteli… Obzvlaste, pokud clovek patra po takovehle “chybe”. Jeste jednou diky

  4. ufak

    Mozna, ze je dobre takove veci pripominat tak dvakrat rocne. Stejne jako equals a hashCode contract

Comments are closed.