Komentáře k článku

Nedokumentované chování vláken a fronty událostí v Javě

Tutoriály vysvětlující, jak psát grafické aplikace v Javě, nám radí, abychom zabezpečili, aby se veškerý kód týkající se grafiky prováděl ve vláknu obsluhujícím frontu událostí. Výklad pak ilustrují na jednoduchých příkladech, které budou vždy fungovat. V tomto článečku bych se chtěl podrobněji podívat na jednu takovou nestandardnost.

Zpět na článek

62 komentářů k článku Nedokumentované chování vláken a fronty událostí v Javě:

  1. problem

    a kde je problem?

    nerozumiem kde je problem, povodny kod je napisany zle a chova sa uplne standardne (nesynchronizovany beh dvoch vlakien a jeho nalsedky)

    1. rudyment

      Re: a kde je problem?

      Běh není nesynchronizovaný. První vlákno zavolá druhé a počká si, až to druhé skončí. Problém je v tom, že to druhé nikdy neskončí, dokud to první neopustí konstruktor třídy. Aby to bylo vidět, tak jsem z ukázky musel tu „synchronizaci“ vyndat, protože pak se vše ozřejmí. To jsem ale v článku zmiňoval – zřejmě příliš nenápadně.

      1. Mips

        Re: a kde je problem?

        Ten běh je nesynchronizovaný. To volání metody „pause()“ přece nemůžete považovat za synchronizaci.

        A celé bych to uzavřel tak, že nelze stavět na nepodložených předpokladech typu „Když to první vlákno na půl sekundy zastavím, tak to druhé vlákno přece za tu dobu stihne udělat tu jednoduchou, rychlou věc!“

        Ano, je to nedokumentované chování. Ale to chování neodporuje specifikaci, ani to není nějaký chyták, na kterém by mohl programátor s velkou pravděpodobností pohořet.

        PS:
        Ještě nějakou dobu (při nejmenším do prvního updatu) bych počkal s vývojem a testováním na JDK 7. Podle ohlasů na internetu obsahuje příliš závažné chyby.

        1. rudyment

          Re: a kde je problem?

          Ta pausa tam je jenom jako náhražka. Jak jsem v článku říkal, v původní verzi (a současně i pak ve druhé verzi v článku) se čekalo, až vyvolané vlákno skončí. Jenomže v původní verzi se druhé vlákno kouslo na volání soukromé metody objektu nedozavedené třídy, a abych přišel na to, proč se to kouše, tak jsem nahradil čekání pausou. Pokud by druhé vlákno nečekalo na to první, tak by to mělo pro synchronizaci bohatě stačit. VM nemá rád nicnedělání, takže nenechává vlákna čekat pro nic za nic.

          Samozřejmě, že pro aplikace používající zaplavu vláken to nebude optimální způsob, ale pro jednoduché aplikace s málo vlákny je to docela dobrá (a relativně rychlá) cesta, jak zjistit, proč ta synchronizace zlobí.

        2. Karel

          Re: a kde je problem?

          No on první problém je v tom, že ono to ve skutečnosti synchronizované je. To první vlákno ten objekt zamkne (protože je v konstruktoru) a pak se diví, že s ním to druhé nemůže pracovat. Doporučuji si všimnout v příkladech na Swing, že buď věci dělají až po volání konstruktoru (a = new b(); a.initialize()), nebo to naopak vyhodí do GUI threadu celé, takže konstruktor už pod EventDispatchThread jede a nemusí žádný další thread vytvářet.

          1. rudyment

            Re: a kde je problem?

            Udělat něco v kontruktoru instance není problém, protože tento konstruktor (přesněji metoda <init>) je metoda úplně stejného druhu, jako jakákoliv jiná instanční metoda, jenom se blbě jmenuje. Vlastní alokaci a předběžnou inicializaci paměti dělá „operátor“ new a po jeho ukončení je už jedno, jestli inicializuji pole v konstruktoru, tj. v metodě <init> nebo v nějaké jiné metodě. Přesunutím inicializace do jiné metody vně metody <init> se proto nic nevyřeší.

            Navíc, konstruktor instance to nekousl. Pro něj alokovaný objekt byl bez problému k dispozici. Kouslo se to proto, že nebyl dokončen konstruktor třídy, tj. metoda <clinit>. Také v závěrečném řešení, kde byla inicializace vyňata z konstrutkoru třídy, vše začalo chodit a odskok z konstrutkoru instance žádné problémy nezpůsoboval.

  2. espinosa_cz

    3 rady od boku

    Přiznám se, že jsem váš článek jen přelétl při večeři a balení na dovolenou, takže jen od boku.

    1)
    Vámi popisovaná metoda lazy initialization je takzvané Double Checked Locking.
    Řídící proměnná musí být VOLATILE, jinak je to bez záruky, a jen pro Java > 5.
    Mimochodem, je to považované za anti-pattern. Doporučené je inicializace pomocí statické vnitřní třídy (má to jen jednu nevýhodu – pokud to selže tam, pak stack trace je opravdu ošklivý)
    http://en.wikipedia.org/wiki/Double-checked_locking

    2)
    Nepopisujete jak jste implementoval vaši vypisovací třídu. Snad ne sdtout a log4j nebo něco podobného. Ladit thready pomocí výstupu na konzoli/souboru je o ničem. Z praxe, řádky logu jsou běžně v jiném pořadí než se skutečně věci udály. Ba i triviální výpis do paměti nemusí být spolehlivě ve správném pořadí. Žádná taková operace se Stringem není atomická. Vždy jej tam šance že se pořadí logů prohodí a nebude odpovídat realitě. Sychronizovat metody? Také nepomůže, Java negarantuje průchod zámkem podle pořadí, ba naopak, je nespravedlivá a ještě se tím chlubí :) Snad zkusit zámky z Javy5 s vynucením fairness, nezkoušel jsem, asi se objeví jiné háčky.

    3)
    Private versus Public. To jste překvapil i mě. No i když ..optimalizér se asi skutečně chová jinak k public a private metodám, k public si toho nemůže dovolit tolik co k private, to se může projevit i ve vztahu k vláknům. Osobně bych se na něco takového nespoléhal.

    Omlovám se, jestli ty rady jsou mimo mísu

      1. rudyment

        Re: 3 rady od boku

        To platí pro vlákna, která se odstartují a pak si žijí vlastním životem. Tady se ale jednalo o akci, který v rámci konstrukce objektu začala a také skočila.

        Vynechávám teď to, že jsem při testování nahradil čekání na ukočnení činnosti obyčejnou pausou, abych zjistil, co se vlastně děje. Ve výsledném kódu je ale vše ukončeno a teprve pak se kontruktor opouští. To druhé vlákno se v něm používá jenom proto, aby se zabezpečilo nekonfliktní vykonání potřebných akcí.

        1. Filip Jirsák

          Re: 3 rady od boku

          To platí pro vlákna, která se odstartují a pak si žijí vlastním životem. Tady se ale jednalo o akci, který v rámci konstrukce objektu začala a také skočila.
          Tohle ale platí jen pro jedno vlákno. Když je vláken víc, může konstrukce v jednom vlákně stále ještě probíhat, a druhé vlákno už mezitím má referenci na objekt a pokouší se s ním pracovat. Což samozřejmě nemusí dopadnout dobře.

          1. rudyment

            Re: 3 rady od boku

            Tady se nic takového nedělo. Nemá smysl rozebírat co by kdyby. Tady šlo jen o to, aby se korektně provedly inicializační akce, které bylo potřeba nějak synchronizovat s jinými akcemi ve frontě událostí. Řešila se proto pouze otázka kdy. Po celou dobu vykonávání inicializace nikdo jiný k odkazu na nedokončený objekt neměl přístup, takže neměl šanci provést nějakou nepravost.

            1. Filip Jirsák

              Re: 3 rady od boku

              Tady ale nešlo o instanci toho jedináčka, ale o třídu. K objektu té třídy se pokoušela přistupovat (dokončit jeho konstrukci) obě vlákna – jenže ten přístup je chráněn zámkem. Takže došlo k uváznutí, když hlavní vlákno drželo zámek a čekalo na dokončení pracovního vlákna, a pracovní vlákno čekalo na získání toho zámku.

              1. rudyment

                Re: 3 rady od boku

                Kdyby to bylo chráněno klasickým zámkem, nefungovalo by to ani s veřejnou třídou. Problém je jinde.

                1. Jirka P

                  Re: 3 rady od boku

                  Mechanismus, kterým vám to deadlockovalo, je popsaný v JLS, kapitola 12.4. Rozdíl mezi public a private metodou popsal už někdo v komentáři (v JLS AFAIK není a jestli takové chování JLS porušuje nevím). Klasický zámek tam není, ale v rámci zjednodušení se to tak dá říct (je tam klasický monitor). Stačí?

                2. Filip Jirsák

                  Re: 3 rady od boku

                  Protože ta veřejná třída asi shodou okolností nepotřebovala přístup ke třídě, tedy nedocházelo k pokusu o inicializaci třídy a čekání na „zámek“ z pracovního vlákna. Jenže jak už jsem psal, na to se v žádném případě nedá spoléhat. Protože se nedá zaručit, že se tam někdo nepokusí sáhnout na danou třídu a tím vlákno nezablokuje. Ostatně, ta privátní metoda vnitřní třídy to také dělá jaksi mimochodem, a až po zásahu kompilátotu, takže z kódu to nevyčtete.

    1. rudyment

      Re: 3 rady od boku

      ad 1)
      To volatile mi tam vypadlo a moc mne to mrzí.

      Že je to jen pro Javu 5 a vyšší jsem neřešil, protože předpokládám, že po těch sedmi letech od příchodu Javy 5 dělají ve starších verzích už jenom ti, co oprašují zděděné systémy, a ti si mohou najít podrobný výklad v uvedené knize i s odkazem na tehdy doporučovaná řešení.

      Co se týka antipatternu, vidím, že asi nejsem stále dost sečtělý, protože literatura, která mi prošla rukama, doproručovala mnou uvedené řešení (samozřejmě s doplněním volatile). Pokud vím, tak za antipattern bylo toto řešení prohlašováno pro verze Javy před pětkou – pak spravili správu paměti a začalo to fungovat.

      Mrzí mne, že jsem neznal řešení přes vnořenou třídu – to se mi opravdu líbí.

      ad 2)
      Nechtěl jsem článek už dál prodlužovat, nicméně použitá třída je k dispozici mezi zdrojovými soubory ke stažení. Jen jsem si teď všiml, že na ně není nikde odkaz, to ale doufam redakce napraví. Pokusím se při té příležitosti dodat i verzi používající vnořenou třídu zmiňovanou v bodu 1.

      Používám samozřejmě nejjednodušší tisk přes System.out a domnívám se, že pro ladění jednodušších programů je zcela dostačující. Synchronizace postačuje, protože to, že Java není v přidělování procesoru vláknům spravedlivá, by se nemělo nijak výrazně projevit, jelikož dokud druhé vlákno nezíská zámek, tak se stejně nemůže prosadit. Jediným problémem by mohl být okamžik, kdy se připravují parametry volané metody – tam by se opravdu mohlo stát, že složený parametr je poskládán částí, z nichž každá byla získána v jiném „soustu“, protože uprostřed skládání byl vláknu odebrán procesor a skládání bylo dokončeno až při příštím přídělu. Na to je třeba pamatovat.

      ad 3)
      Obávám se, že v tuto chvíli se optimalizátor neuplatní, protože program se spustí jenom jednou a pokud vím, překladač s optimalizátorem vstoupí do hry až pro opakovaně používaný kód. Podle mne bude ve VM nastavena jakási spolupráce mezi interními kódy pro správce vláken a zavaděč tříd, která při přechodu mezi verzemi 5 a 6 změnila pravidla.

  3. x22

    private vs. public

    Na pochopenie, preco public funguje je nutne vediet, ako to kompilator skompiluje v pripade private metody (napr. priklad Tst_1A)

    Je na to nutny jeden trik a to, ze kompilator do vonkajsej triedy prida dalsu methodu s podivnym nazvom (napr. access$000), ktora ma implicitny pristup a vnutri vola tu privatnu. Z vnorenej triedy sa zavola tato specialna metoda (JVM „nevie“, ze ide o vnorenu triedu, takze priame volanie private metody vonkajsej triedy nedovoli).

    Ta specialna metoda je navyse staticka, a tak jej volanie musi cakat na inicializaciu triedy, pretoze ta bezi v hlavnom vlakne a este neskoncila.

    Volanie instancnej metody cakat nemusi (http://java.sun.com/docs/books/jls/third_edition/html/execution.html#12.4.1), tym padom public metoda tento problem nema (nepotrebuje trik s pridanim access$000).

    (access$000 je vidno vo vypise „javap <meno triedy>“ alebo „javap -c <meno triedy>“. <clinit> tiez, aj ked nie pod menom <clinit>.)

    1. rudyment

      Re: private vs. public

      Díky za připomenutí. O této pomocné metodě jsem četl, ale protože ji dekompilátory běžně nezobrazují, tak jsem ji zasunul do pozadí a vůbec si neuvědomil, že právě v této situaci se stane klíčovou pro vysvětlení celého chování.

      Vaše odpověď navíc vyčnívala z davu tím, že šla rovnou k věci a nepohazovala obecnými principy ignorujíce skutečnost, že pro každé pravidlo můžeme najít situace, kdy je třeba danou zásadu porušit, ani nepředkládala nějaké podivuhodné závěry, u nichž stačilo, aby si je autor zkusil naprogramovat a uviděl by, že nejsou obecně platné.

      Ještě jednou díky.

  4. Leoš

    Špatně udělaná líná inicializace

    Tak schválně: kdo první najde chybu v metodě „public static Tst_2 getInstance()“? Teda aspoň druhý, první jsem ji našel já. ;-)

    1. Leoš

      Re: Špatně udělaná líná inicializace

      Mea culpa, měl bych důsledněji pročítat diskuse, je vidět že předřečníci už na ten nepovedený Double Checked Locking upozornili. Tak aspoň ve stručnosti doplním co se bez toho volatile může stát (bez ohledu na verzi Javy): jeden thread nastaví hodnotu toho non-volatile fieldu na novou hodnou (!=null) a druhý thread tuto hodnotu uvidí dříve, než doběhne konstruktor toho objektu který je do toho fieldu přiřazený. Od Javy 5 se to dá opravit označením toho fieldu jako volatile, v předchozích verzích Javy byl ještě jiný memory model kde ani toto nefungovalo.

      1. Filip Jirsák

        Re: Špatně udělaná líná inicializace

        Tím se opraví problém té pozdní inicializace jedináčka (tj. členské proměnné singleton). Jenže v té vnitřní třídě implementující Runnable je implicitně odkaz na třídu Tst_2, který se opět předává do jiného vlákna. Takže volatile singleton zajistí, aby reference na částečně inicializovaný objekt neutekla do jiného vlákna, ale ta samá reference uteče přes Tst_2.this.

        1. Leoš

          Re: Špatně udělaná líná inicializace

          Komu není rady, tomu není pomoci… Pokud se někdo takhle moc snaží tu inicializaci udělat špatně, tak se mu to nakonec povede. Zrovna tady by pomohlo definovat xxx jako final – pak vám už překladač vynadá že je ten konstruktor napsaný špatně. Já třeba final používám skoro všude, ale z cizího kódu často vidím že se final skoro nepoužívá, což mi připadá škoda.

          A abych byl aspoň trochu konstruktivní – vím, co to znamená Event Dispatch Thread a jaká přináší omezení. Proč ale není v EDT atomicky celé tvoření toho singleton objektu, proč je do EDT vystrčená jen ta metoda initializaton()?

          1. rudyment

            Re: Špatně udělaná líná inicializace

            Zkonstantnění odkazu na jedináčka a vystrčení konstrukce celého objektu nepomůže, protože druhé vlákna se kousne při čekání na dokončení konstrutkoru třídy.

            Jak jsem naznačil v článku: jsou dvě možnosti. Buďto zruším označení dané metody jako private, anebo oželím to, že odkaz na jedináčka bude konstanta.

            To druhé je výhodnější, protože při použití reflexe můžeme ponechat odkaz na jedináčka jako konstantu jejíž hodnotu nastavíme trochu nestandardně anebo ji nastavit využitím Future.

            Úplně nejlepší je ale řešení přes vnořenou (embedded) třídu, které tu někdo zmiňoval. Nicméně opět: aby se to rozběhlo, musí se to zavolat mimo konstrutkor třídy.

            1. Filip Jirsák

              Re: Špatně udělaná líná inicializace

              Řešení je jediné – zařídit, aby inicializace v jiném vlákně neblokovala dokončení konstruktoru třídy. To se dá zařídit dvěma způsoby – buď odloženou konstrukcí jedináčka, nebo jeho odloženou inicializací. Já bych preferoval druhou variantu, tj klasické klasické private static final singleton = new, a teprve v getInstance() otestovat, zda je objekt inicializován, a pokud ne, inicializovat jej.

              Další možnost – zařídit, aby v průběhu inicializace objektu z jiného vlákna nebyl žádný požadavek na zamčení třídy, a v hlavním vlákně pak čekat na dokončení inicializace instance – není řešení, protože to mimo ty nejjednodušší případy spolehlivě zařídit nejde. Na tom případu private/public je vidět, jak snadno může dojít k tomu, že vlákno bude potřebovat zámek na té třídě. A nikdo nezaručí, že desítky dalších takových záludností nečekají v kódu odložení do AWT vlákna, nebo že je tam teprve někdo nepřidá. „Nesahat na třídu“ není nic, s čím by se běžně počítalo, takže se třídy běžně používají při logování, v keších, při přetypování a na spoustě dalších míst.

              1. rudyment

                Re: Špatně udělaná líná inicializace

                Odložená inicializace objektu jedináčka se mi nelíbí, protože kvůli ní je třeba deklarovat všechny inicializované atributy jedináčka jako nekonstantní. Navíc se tím tělo konstruktoru zbytečně rozděluje do dvou metod, což se sice někdy hodí, ale v tomto příkladě to považuji za zbytečné.

                Já bych preferoval onu odloženou konstrukci.

            2. Leoš

              Re: Špatně udělaná líná inicializace

              A proč ignorujete tu třetí možnost kterou jsem nabídnul? Jde o podvolení se architektuře používající Event Dispatch Thread tím, že nebudu konstrukci objektu dělit mezi více threadů (což považuji za jádro celého problému – část dělá EDT a část jiný thread), ale místo toho využiji EDT k zkonstruování celého mého objektu (EDT proto že předpokládám, že při inicializaci bude třeba pracovat s jinými komponentami, které mají omezení na EDT). Tento postup se obvykle označuje jako thread confinement a jde o základní princip na kterém stojí EDT.

              1. Leoš

                Re: Špatně udělaná líná inicializace

                Ještě mě napadla další možnost: singletony nevázat na Java třídy (tedy pomocí modifikátoru static), ale podvolit se nějakému dependency injection containeru (např. Spring) který si pro svou jednu instanci (u Springu ApplicationContext) už pohlídá aby požadovaný objekt udělal pouze jednou, normálně záznamy ve svých objektech, žádný „static“.

    2. ijacek

      Re: Špatně udělaná líná inicializace

      Uz drive me napadlo, ze asi neni zaruceno na vsech platformach, ze hodnota pointeru se meni „najednou“. Nebo je to v necem jinem?

      1. thingol

        Re: Špatně udělaná líná inicializace

        Změna reference je v Javě vždy atomická. Problém je v tom, že pokud máte v kódu několik (obyčejných) zápisů do proměnných, tak není obecně zaručeno, v jakém pořadí budou z ostatních vláken viditelné. Přeuspořádat je může jak překladač, tak procesor (x86 používá tzv. total store order paměťový model, takže až na několik výjimek ostatní procesory/jádra vidí zápisy v tom pořadí, v jakém se nachází v toku instrukcí, ale např. u ARMu tohle neplatí). V tomhle konkrétním případě se může stát, že ta změna reference bude vidět dříve, než některé zápisy provedené při konstrukci toho objektu.

  5. Filip Jirsák

    konstrukce objektu není atomická operace

    Podle mne je to akorát trochu zašmodrchaný příklad ukazující to, že konstrukce objektu není atomická operace, jak spousta programátorů předpokládá. Pokud mám v Javě kód

    Object obj = new Object();

    je zaručeno, že bude objekt plně inicializován a teprve pak poběží další kód v daném vláknu. Jakmile mi může ta reference utéct do jiného vlákna, bez synchronizace není zaručeno vůbec nic.

    První „řešení“ podle mne vůbec není řešení – volání privátní metody je velice jednoduché, při volání public metody se obecně ta metoda musí nejprve najít, takže to zřejmě bude trvat dýl a nejspíš tam budou i nějaké synchronizační instrukce. Takže chování příkladu se změní jako vedlejší efekt, ale není to nic zaručeného. Pokud je to ovlivněno pouze časováním, může to tu samou chybu způsobit na počítači s jiným počtem jader procesoru nebo jiným taktováním. Pokud je to ovlivněno implementací volání veřejných metod, může to opět rozbít „jen“ jiná implementace JVM nebo nějaká optimalizace.

    Ve druhém řešení není nikde vidět, že by člen singleton byl definován jako volatile, takže to opět není spolehlivé řešení – getInstance() může vrátit neinicializovaný nebo částečně inicializovaný objekt. Jak už tu někdo napsal, pokud se použije volatile, od Javy 5 už je zaručeno, že tam bude paměťová bariéra, takže nejprve doběhne celý kód inicializace objektu, a teprve pak se k referenci dostanou jiná vlákna. Takže použití volatile by spravilo odloženou inicializaci singletonu, ale pořád by myslím neřešilo to jiné vlákno, v jehož rámci běží kód v metodě initialization(). Každopádně bez volatile vám pořád může getInstance() vrátit neinicializovaný objekt.

    1. Maaartin

      Re: konstrukce objektu není atomická operace

      > První „řešení“ podle mne vůbec není řešení

      Jako jeho autor s timto musim souhlasit… nahodou jsem to zkusil a ono se to „vyresilo“, ale nikomu bych to neradil.

      Namisto volatile bych asi pouzil tridu, takovou ze v ramci jejiho natazeni se ten singleton nainicializuje. Pripada mi to jako nejjednodussi a nejbezpecnejsi reseni (ale to muze byt tim ze neco nechapu dost dobre).

      1. Filip Jirsák

        Re: konstrukce objektu není atomická operace

        Souhlasím, to je nejlepší způsob vytvoření singletonu. Ale nesmí se v rámci konstrukce toho objektu z jiného vlákna přistupovat k té třídě, protože pak dojde k uváznutí – první vlákno bude čekat na dokončení inicializace instance singletonu v druhém vlákně (a bude držet zámek na třídě), a druhé vlákno bude čekat na dokončení inicializace třídy singletonu v prvním vlákně (které drží příslušný zámek).

    2. v6ak

      Re: konstrukce objektu není atomická operace

      Ona je to věc kompilace javy (javac, Eclipse Java Compiler apod.), ne JVM, ledaže by běžné způsoby implementace Singletonu byly podle definice broken. Někdo to tu již vysvětlil, kompilátor si zde v případě private pomáhá public synthetic static metodou, aby obešel omezení JVM (ta inner classes dnes prakticky nepodporuje, ty řeší kompilátor), čímž způsobí určitou synchronizaci (statické metody čekají na inicializaci třídy) a tím deadlock. Specifikace jazyka dost možná umožňuje oboje, ale tyto edge cases neznám.

      Dovedu si představit spíše opačnou změnu – místo statických metod se začnou používat instanční metody (prefixované názvem třídy, aby se nemlátili předci s pototmky), což způsobí konzistentnější chování, tedy jakékoli volání instanční metody jakéhokoli objektu nebude čekat na dokončení inicializace třídy.

      V praxi bude asi největším problémem to, že se někdo bude divit, proč to není privátní, a zprivatizuje to. Nebo někdo použije něco podobného s privátní metodou. Atd atd. Jakýkoli kód, kde si programátor může myslet, že ho dostatečně dobře chápe, ale ve skutečnosti mu tam něco uniká (ať už jeho vinou, nebo ne), může být velmi problematický, zvláště u zrádných problémů „podle počasí“.

      Malá předpověd na závěr: Ačkoli jsem si mnohdy nejistý i u předpovědí pod pět let, vsadil bych se, že během následujících patnácti let nepřestane trik s public (popř. package-private) v Javě fungovat. Kompilátor by prostě musel fungovat nepochopitelně krkolomně. Jen možná už nebude považován za trik, protože bude (možná) fungovat (a možná bude i zaručena) varianta s private.

      1. Filip Jirsák

        Re: konstrukce objektu není atomická operace

        Myslím, že ten trik s veřejnou metodou nefunguje už teď. Vždyť ta soukromá metoda způsobila jedinou věc – způsobila zavolání (uměle vytvořené) statické metody, to ale vyžaduje inicializovanou třídu, takže se pracovní vlákno zastavilo a čekalo(1) na dokončení inicializace třídy. Proto použití invokaAndWait způsobilo deadlock (donutilo to čekat hlavní vlákno na dokončení konstruktoru, takže na dokončení pracovního vlákna). Změnou na veřejnou metodu se odstranilo to volání statické metody, takže pracovní vlákno nemuselo čekat (odstranilo se čekání(1)) a proběhlo „rychleji“. Takže stihne nainicializovat xxx dřív, než se k němu dostane hlavní vlákno.

        Jenže nikde není zaručeno, že to pracovní vlákno bude opravdu vždy rychlejší – stačí přidat pár řádek kódu, a nestihne to. Stačí přidat pauzu, a nestihne to. Stačí zavolat jinou statickou metodu, opět se objeví čekání(1) a vlákno to zase nestihne. Dobře, to všechno jsou zásahy do kódu, možná jste myslel, že ten trik bude fungovat s nezměněným kódem. Ale ani to není pravda, protože to pracovní vlákno se může zadrhnout z nějaké vnější příčiny zcela mimo JVM – stačí, že mu OS naplánuje míň procesorového času, a to vlákno to zase nestihne. Ty případy se změnou kódu se dají ověřit; zlý OS, který nedá vláknu procesor, se dá nasimulovat v debuggeru. Může někdo za domácí úkol :-) udělat některou tu úpravu a ověřit, že případy „nezdařilo se“ nebo „těsně vedle“ budou nastávat i s tou veřejnou metodou.

        Nebo-li důvod, proč to s tou veřejnou metodou funguje častěji, je ten, že se změní relativní „rychlost“ obou vláken. Akorát to funguje opačně, než jsem tu někde po zběžném prohlédnutí navrhoval – veřejná metoda je v tomto případě rychlejší, protože volání privátní ve skutečnosti znamená volání statické metody a čekání na plnou inicializaci třídy.

        1. v6ak

          Re: konstrukce objektu není atomická operace

          Pokud jsem to pochopil, tak to byla ona synchronizace, kterfou byl deadlock podmíněn. Pak jejím odstraněním odstraníme problém. Ale možná jsem si to z mobilu příliš málo přečetl.

          1. Maaartin

            Re: konstrukce objektu není atomická operace

            Taky si myslim, ze tim zverejnenim se odstranilo cekani na initializaci tridy a tim i duvod deadlocku. U me to fungovalo snad zcela deteministicky a myslim ze se to casovanim podelat nemuze. Coz samozrejme neznamena ze bych se na to chtel spolehat.

            1. Filip Jirsák

              Re: konstrukce objektu není atomická operace

              Ano, odstranil se důvod deadlocku, ale když tam není žádná synchronizace, není nikde zaručeno, že se ten objekt plně inicializuje dřív, než se někde použije – tj. vrátil se zpět ten původní problém, kvůli kterému se to celé začalo řešit.

              Prodlužte dobu běhu pracovního vlákna před začátkem inicializace (třeba na začátek metody initialize() přidejte 2sekundovou pauzu), a uvidíte, že to začne zase deterministicky padat na NPE, protože xxx == null.

          2. Filip Jirsák

            Re: konstrukce objektu není atomická operace

            On ten článek popisuje několik problémů, různé úpravy a není vždy zcela jasné, o které variantě se kdy mluví.

            Pokud se objekt inicializuje v jiném vlákně, musí tam být vždy nějaká synchronizace – bez té se nedá zaručit, že se někde nepoužije částečně vytvořený objekt. Odstranění synchronizace samozřejmě odstranilo problém s deadlockem, ale zase vrátilo do hry problém s částečně inicializovaným objektem. Správné řešení je mít tam synchronizaci, ale takovou, aby nedocházelo k deadlocku.

            1. Jirka P

              Re: konstrukce objektu není atomická operace

              V tom druhém řešení byl ten invokeAndWait, který by synchronizaci mohl řešit (mohl protože v dokumentaci se o tom nic nepíše, bez toho to volání ale nemá moc smysl). Jinak by se musel použít invokeLater + monitor.

  6. René Stein

    Klasický deadlock?

    Dobrý den,
    v javě již delší dobu aktivně neprogramuji, ale pokud se dobře pamatuji, tak váš příklad je klasickou ukázkou „statického inicializačního fiaska“ v Javě.

    1) Jestliže je spuštěna statickou inicializace třídy a současně se z instančního konstruktoru stejné třídy volaného v rámci statické inicializace pokusíte spustit druhé vlákno, které se pokusí o rekurzivní statickou inicializaci, tak chování je takové (ve vašem případě s pauzou – jinak musíte skončit v deadlocku), že druhé vlákno zjistí, že statická inicializace běží a opakovaně ji nespustí. Samotná statická inicializace je vždy vykonána v sekci synchronized.

    Řekl bych, že chování se dá odvodit i ze specifikace Javy.
    http://java.sun.com/docs/books/jls/third_edition/html/execution.html#12.4.2

    2) Narazil jste pravděpodobně na jedno z omezení statické inicializace, kdy modifikátory public a private se mohou chovat rozdílně což popisuje kolega výše. O statickém inicializačním fiasku se mluví lavně v C++, ale to neznamená, že u Javy nebylo identifikováno velké množství problémů.
    Viz starší studie ftp://www.eecs.umich.edu/groups/gasm/javainit.pdf
    Stále bych řekl, že ale toto chování závisí na kompilátoru Javy, class loaderu, počtu procesorů a jader na procesoru apod.

    3) Sám synchronizuete v kodu přes class. Přes globálně viditelný objekt (zde deskriptor třídy class) se nikdy nesmí synchronizovat. Objekt, nad kterým zamykáte, musíte mít plně pod kontrolou právě proto, abyste eliminoval riziko deadlocků.

    4) Podle mě by bylo vhodné mít pomocný objekt pro lazy inicializaci podobný objektu Lazy<T> v C# http://msdn.microsoft.com/en-us/library/dd642331.aspx – případně i s podporou dalších vláken.

    5) Terminologie v článku je poměrně matoucí – píšete o tom, kolik času dáte na vykonání vlákna, mluvíte o synchronizaci, ale přitom, z výukových důvodů a kvůli onbejití deadlocku, jen zapauzujete provádění vlákna. To je velmi matoucí, protože nikdy není garantováno, kdy thread scheduler vlákno spustí, vliv na přípravu exekuce má i počet procesorů a počet jader na procesoru. To znamená, že váš příklad neukazuje nic – a může kolabovat i kdyby statický inicializér byl napsán správně.

    1. Petr Lazecky

      Re: Klasický deadlock?

      Mas pravdu. Je to jednoznacne deadlock protoze ten kod kombinuje nekolik advanced veci jazyka Java. Kod pouziva closure v konstuktoru, kde se vytvari anonymni trida Runnable, jejiz instance (closure) zavola v jinem vlakne metodu initialize() coz zpusobi deadlock, protoze puvodni vlakno, ktere dela inicializaci te closure, jeste nedobehne (nekdy se to stihne, nekdy ne, proto to chovani).

      Problem ze statickou definici promennych (objektu), jak je notoricky zname z C++ se zde dle meho nazoru neuplatni, protoze ten problem se tyka predevsim *poradi* initializace statickych objektu a to jeste v pripade, kdy se jeden staticky objekt zacne odkazovat na jiny. V nasem pripade je staticky objekt jen jeden, takze „no issue“.

      Stejne tak se neuplatni problemy s Double Check Locking protoze kod se nesnazi promenou testovat a pak nastavovat. Tyto problemy (Double Check Locking a poradi staticke inicializace) souvisi s necim uplne jinym a na vysvetleni tohoto problemu nemaji, dle meho nazoru, vliv.

      Pokud zde mluvim o „closure“, tak tim mam na mysli koncepcni closure, to jest zpusob jak je anynomni trida zkompilovana do byte kodu JVM a nikoliv Closure, ktere jsou soucasti Javy 7 (aka lambda expressions). Closure zde chapu jako schopnost jazyka zachytit kontext definice anonymni tridy.

      Na zaver snad maly nazor do pranice. Chapu, ze ma tento kod demonstrovat problem, ale takovyto kod je neco, co profi programator urcite nenapise. Predevsim mam na mysli to, ze se kod nedrzi zakladniho pravidla jak psat kontruktory, a to hlavne konstruktory staticke (vyjimka vyhozena ze statickeho konstruktoru zpusobi nedostupnost celeho typu). Zahajovat v konstruktorech I/O operace, nedejboze vytvaret vlakna ci volat dalsi operace, ktere mohou vyhazovat vyjimky povazuji za naprosto spatny OO design nebot konstruktor ma slouzit pouze a jen k pocatecnimu nastaveni invariantnich hodnot dane instance tridy ale nikoliv k inicializaci instance (na tu je treba samostatna metoda Init()).

      A jeste jedna „drobnost“ – pouzivat pro ladeni vicevlaknovych aplikaci vypis na konzoli ci do souboru vede k vysledkum, ktere se lisi od kodu, ktery tyto logy nema (protoze tyto IO operace jsou nejen casto synchronizovane na urovni OS ci JVM ale dokonce vnaseji do kodu „zadouci“ spozdeni a tim zpusobi jine chovani kodu diky vedlejsim efektum techto logu v podobe jineho „casovani“ instrukci). To jen na okraj…

      1. Rene Stein

        Re: Klasický deadlock?

        Souhlas, já tam vidím hlavně tento problém.

        1) Sáhne na třídu a spustí její statickou inicializaci (sekce static).
        2) V sekci static vytvoří instanci třídy (ze stejné třídy, kde je spuštěna statická inicializační sekce).
        3) V konstruktoru spustí v jiném vlákně kód, který by způsobil rekurzivní statickou inicalizaci – ta samozřejmě neprojde. Pokud v prvním vlákně měl „Wait“ na dokončení druhého vlákna, tak musel pravidelně dostávat deadlock (v diskuzi to mimochodem potvrzuje, kvůli tomu použil de facto „Sleep“ prvního threadu)

        Z C++ jsem si vypůjčil termín statické inicializační fiasko, protože sice nejde o cirkulární inicializaci proměnných a závislosti na pořadí inicializace, ale statická inicializace třídy stejně korektně nedoběhne – jde o závislost na počtu vláken iniciujících rekurzivní spuštění statické inicializace v jedné třídě, což Java nepovolí, ale má to vedlejší efekty popisované v článku (z mého pohledu ne moc překvapivé)

        Double-check tam samozřejmě pro demonstraci problému zase tak zajímavý není.

      2. Heron

        Re: Klasický deadlock?

        <blockquote>
        Na zaver snad maly nazor do pranice. Chapu, ze ma tento kod demonstrovat problem, ale takovyto kod je neco, co profi programator urcite nenapise. Predevsim mam na mysli to, ze se kod nedrzi zakladniho pravidla jak psat kontruktory, a to hlavne konstruktory staticke (vyjimka vyhozena ze statickeho konstruktoru zpusobi nedostupnost celeho typu). Zahajovat v konstruktorech I/O operace, nedejboze vytvaret vlakna ci volat dalsi operace, ktere mohou vyhazovat vyjimky povazuji za naprosto spatny OO design nebot konstruktor ma slouzit pouze a jen k pocatecnimu nastaveni invariantnich hodnot dane instance tridy ale nikoliv k inicializaci instance (na tu je treba samostatna metoda Init()).
        </blockquote>

        +1 Od Pecinovského bych takový kód ani nečekal. Při čtení článku jsem měl pořád dojem, že musí přijít odstavec „Tak milé děti, a teď si ukážeme, jak to udělat správně a přehledně“. Bohužel, i mistr tesař se utne.

  7. Leoš

    1 rada od boku

    Každý kdo se v Javě pokouší o vícevláknové programování by si povinně měl prostudovat Java memory model, ušetří si tím spoustu času stráveného při složitém ladění divně se chovajících programů. A může pak při psaní článků volit místo nadpisu „Nedokumentované chování vláken“ například pravdivější nadpis „Chování vláken dokumentované v kapitole 17 Java Language Specification“. ;-)

    1. rudyment

      Re: 1 rada od boku

      Asi jsem slepý nebo nedovtipný, ale já jsem tam zmínku o popsaném chování nezaregistroval. Přesněji: nevšiml jsem si, že by se tam z něčeho dalo odvodit, že se bude přístup k soukromým a nesoukromým metodám lišit a že se přístup k soukromým metodám bude lišit podle toho, je-li již ukončen konstruktor jejich třídy či nikoliv.

      Možná, že jsem danou kapitolku četl příliš zběžně. Uvítal bych proto (a jistě nejen já) najakou přesnější lokalizaci tvrzení, z nichž lze popsané chování odvodit.

      1. Filip Jirsák

        Re: 1 rada od boku

        Podle mne ta odlišnost neplyne vůbec z toho, zda jde o soukromou nebo veřejnou metodu. Ten rozdíl soukromá/veřejná způsobí trochu jiné instrukce, jiné časování souběhu vláken, jinou práci s pamětí. A něco z toho jako nezamýšlený a nezaručený vedlejší efekt způsobí to jiné chování souběhu vláken.

        Nebo-li je to něco podobného, jako krátké zapauzování toho vlákna. Ve vaší testovací konfiguraci může pozdržení vlákna (pauzou nebo zaměstnáním procesoru hledáním správné implementace public metody) způsobit, že druhé vlákno stihne doběhnout dál a chyba se neprojeví. Když to pak pustíte v jiné konfiguraci, může být pauza moc krátká, procesor bude mít větší cache a najde rychleji správnou implementaci metody nebo překladač bude inteligentnější, zjistí, že daná metoda nikde přetížená není a přeloží ji stejně rychlou, jako privátní metodu (případně její kód rovnou vloží na místo jejího volání), a může se to zase chovat stejně, jako by ta metoda byla privátní.

      2. Leoš

        Re: 1 rada od boku

        Ta odlišnost v přístupu k soukromým a nesoukromým metodám je pravděpodobně implementační detail který ve vašem případě náhodou platí. Není dobré na tuto náhodu spoléhat. Pokud chcete mít program skutečně robustní, je třeba v Javě programovat v souladu se specifikací (kterou je tedy třeba mít nastudovanou) a neexperimentovat s tím že v jedné konkrétní implementaci Javy něco funguje nějakým konkrétním způsobem.

        1. Aminux

          Re: 1 rada od boku

          Nemyslím, že by autor na takovéto náhody spoléhal. Zrovna u tohoto člověka o tom pochybuji. Špíš to dělá dojem, že náhodou se stkal s takovouto situací a nevěděl si rady.

        2. rudyment

          Re: 1 rada od boku

          Také jsem hned říkal, že se mi to řešení s odprivatizováním metody nelíbí. Mohlo by nám ale napovědět něco o tom, proč k tomu došlo.

          Stále ale čekám na odpověď na otázku, kde je podle vás v 17. kapitole JLS napsáno něco, z čeho se dá odvodit popsané chování, abych pak mohl podle vašeho návrhu článek nazvat „Chování vláken dokumentované v kapitole 17 Java Language Specification“

            1. rudyment

              Re: 1 rada od boku

              Vynechám-li, že odkaz vede do starší verze JLS platné do Javy 1.4 včetně tak se o v dané pasáži o probíraném problému nemluví.

              Příčiny vysvětlil podle mne naprosto přesně ve svém příspěvku X22. Ale tento důvod v dané pasáži nijak zmíněn není. To je interní (a proto ve specifikaci nezmiňovaná) záležitost překladače, jak se vypořádá s problémem volání privátní metody v jiné třídě, a popisovaný problém se synchronizací vláken je jeho důsledkem.

              1. kink

                Re: 1 rada od boku

                To samozrejme plati, nicmene to nemeni fakt, ze uvedeny priklad shorel na spatne synchronizaci. Ta, pokud by byla dobre napsana, by fungovala nezavisle na access levelu.
                Pokud vezmu tridu po tride problemy jsou nasledujici:
                Tst_1 – nechavate uniknout instanci Tst_1 pomoci anonymni tridy Runnable a tu jeste spoustite nekdy v budoucnu, takze na vysledek metody initializaton se nelze spolehnout
                Tst_1A – to same v blede modrem, s tim ze jeste nechate konstruktor cekat 500ms, coz ovsem nic neresi, co kdyz je v EventQueue jeste dalsi milion Runnable na zpracovani?
                Tst_1B – opet unik instance, nikde neni zaruceno, ze po dokonceni konstruktoru se trida korektne inicializuje, protoze – co kdyz bude Thread prerusen?
                Tst_2, Tst_2A – unik instance, double-checked locking v kombinaci s non-volatile promennou – kdyby singleton byl definovan jako volatile, pak by getInstance byla korektni. K tomu jeste pridam poznamku, ze tento typ chytre synchronizace nema v novejsich JVM (>=1.5) smysl, stacilo by to nahradit v podstate stejne levnou synchronizaci cele getInstance metody.
                Tst_2B – velmi podobne Tst_1B, ale cekame na dokonceni pomoci join.

                1. flv

                  Re: 1 rada od boku

                  To je zajimave od 1.5 >= je synchronized na metode stejne narocne jako doble checked locking ?

                  Muzete to rozvest ?

                  1. kink

                    Re: 1 rada od boku

                    Pardon, to jsem se upsal, 1.5 tam nepatri, co jsem chtel rict je, ze rozdil vykonu je zanedbatelny pokud srovname synchronizovanou metodu a double-checked locking na aktualnich verzich JVM.

                    1. X

                      Re: 1 rada od boku

                      Od 1.5 se změnil memory model, volatile se začalo chovat užitečněji (konečně funguje happens-before, čtení volatile synchronizuje hlavní paměť do paměti threadu, zápis volatile synchronizuje paměť threadu do hlavní paměti), díky čemuž například konečně funguje DCL. Použití volatile se tím ale zdražilo. Dále se následnými optimalizacemi zlevnila práce se zámky (synchronized) se kterými pracuje pouze jedno vlákno. Sečteno, podtrženo: volatile už dnes není o moc levnější než nezatížené zámky.

  8. Bjarne

    Chyba v článku

    Domnívám se, že do výpisu zdrojových kódů vnikl šotek, protože ve verzích se sync. voláním (invokeAndWait(Run­nable)) vidím stále invokeLater.

    1. rudyment

      Re: Chyba v článku

      Děkuji za upozornění, tak to už je druhá chyba (po několikrát zmíněném volatile). Omlouvám se a už ji běžím napravit.
      Naštěstí ve zdrojových kódech není, zanesl jsem ji při přepisování programů do článku.

  9. Bjarne

    Šotek číslo 2?

    Už první verze kódu obsahuje tu metodu „initialization()“ jako veřejnou. Poté verze doplněná pouze o kontrolní výpisy už ji obsahuje jako soukromou.

  10. Viktor

    AWT???

    ať se na mě autor nezlobí, ale psát dnes o AWT je jako historický exkurz. Tento typ aplikací nemá budoucnost …; když už UI, tak co se třeba zaměřit na GWT, Vaadin, atd. ?

    1. Bjarne

      Re: AWT???

      Kdybyste nebyl ignorant, tak byste po přečtení článku věděl, že článek není o AWT, ale je obecný (multithreading, inicializace). Navíc Swing (postavený nad AWT) není pasé a GWT/Vaadin má s desktopem pramálo společného, jelikož to jsou frameworky pro web UI ;)

  11. marvertin

    Konstruktor konstruuje objekt
    Konstruktor je od toho, aby stvořil objekt. Startovat v konstruktoru vlákna je prasárna stejně jako jakékoli jiné vedlejší efekty konstruktoru.

Napsat komentář

Tato diskuse je již příliš stará, pravděpodobně již vám nikdo neodpoví. Pokud se chcete na něco zeptat, použijte diskusní server Devel.cz

Zdroj: https://www.zdrojak.cz/?p=3525