Přejít k navigační liště

Zdroják » Různé » Nedokumentované chování vláken a fronty událostí v Javě

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

Články Různé

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.

Článek by mohl zajímat nejenom ty, kteří tvoří GUI, ale i ty, kteří začínají pracovat s vlákny a chtěli by se o nich dozvědět trochu víc. Je to totiž také tak trochu ukázka, jak se může lišit chování vláken při jednotlivých spuštěních programu.

1. Předehra

Potřeboval jsem v jedné aplikaci definovat aplikační okno jako jedináčka (singleton). Standardně doporučovaný postup říká, že optimální je definovat ve třídě jedináčka konstantní statický atribut odkazující na tuto její jedinou instanci a v rámci jeho deklarace daný atribut hned také inicializovat. Zařídil jsem se podle toho doporučení, ale jednou za čas se inicializace nepodařila a program se kousl. Frekvence kousání přitom záležela na počítači a operačním systému. Někde se program zasekl tak v jednom z padesáti pokusů, jinde musel člověk program pětkrát spustit, než se konečně rozběhl.

Řekl jsem si, že to bude tím, že jsem neuposlechl jiné doporučení, a to že veškerý kód týkající se grafiky a zavánějící tím, že se bude něco vykreslovat, by se měl provádět ve speciální frontě událostí, kterou definuje grafická knihovna AWT. Upravil jsem tedy program, a ejhle – přestalo to chodit úplně. Nejprve jsem podezříval vlastní neschopnost, ale posléze jsem přišel na to, že vedle ní vstoupily do hry i některé nedokumentované vlastnosti vláken a z nich odvozené vlastnosti fronty událostí a ji obsluhujících metod.

Článek není určen pro úplné začátečníky, a proto bych před tím, než se pustím do demonstrace svých programů a analýzy jejich chování, nejprve shrnul, o čem by měl mít čtenář tohoto článku alespoň povědomí:

– Měl by vědět, jak se v Javě pracuje s interními třídami (embedded classes, inner classes) a především pak s anonymními třídami (anonymous classes).

– Měl by tušit, co jsou to vlákna, a mít základní povědomí a práci s nimi.

–  Kupodivu nemusí mít žádné znalosti grafických knihoven, protože v zájmu obecnosti budu pracovat s běžnými objekty, které nijak nesouvisí s grafikou. Může se tu ale dozvědět, jak s takovými objekty pracovat.

1. 1. Třída s ladícími tisky

Protože jsem potřeboval analyzovat chod programů běžících v několika vláknech, připravil jsem si pro kontrolní tisky piditřídu ThreadMessages se čtyřmi metodami:

–  void msgS(String)
Metoda se volá po vstupu do zavolané metody, abychom oznámili začátek vykonávání této metody a současně odsadili následující tisky. Tím se zpřehlední výstup, protože pak lze rychle najít, kdy daná metoda končí svoji práci a předává řízení zpět metodě volající. Teoreticky bych sice mohl odvodit název zavolané metody z informací na zásobníku odkazů, ale získání informací ze zásobníku odkazů je časově poměrně náročná operace (někdy příště si to můžeme ukázat), a proto jsem dal přednost uvedení názvu zavolané metody v parametru.

–  void msg (String)
Tato metoda se používá pro běžné kontrolní tisky, přičemž respektuje zobrazování úrovně zanoření nastavené metodami msgS a msgF.

–  void msgF(String)
Metoda se volá před výstupem z metody, abychom tuto skutečnost oznámili a aby se další tisky opět přisadily.

–  pause(int)
Metoda jenom uspí program na zadaný počet milisekund. Je zde definovaná proto, abych nemusel ošetřovat výjimku metody Thread.sleep(lon­g).

Struktura řádku s kontrolním tiskem (pro přehlednost používám pouze jednořádkové tisky) je následující:

– Každý tisk začíná počtem vteřin od spuštění programu,

–  za nímž následuje znak indikující aktuální vlákno (hlavní vlákno je indikované znakem –, další spuštěné znakem +, další zatím nepotřebujeme),

–  za nímž je série většítek naznačujících, jak hluboko je v daném vlákně vnořeno volání metody, ze které byl daný řádek vytištěn

–  a na konci je pak zpráva předaná v parametru, přičemž metoda msgS předsazuje před tuto zprávu text „Started:  „ a metoda msgF text “Finished: „

Výstup těchto kontrolních tisků vypadá např. následovně:

0,002 - Started:  main() # Tst_1.Main
0,019 - > Started:  testMain()
0,021 - > > Started:  <clinit> # Tst_1
0,021 - > > > Started:  Constructor
0,051 - > > > > Constructor: Start to sleep
0,052 + Started:  invokeLater - run()

Budeme-li analyzovat zobrazený děj, zjistíme, že:

– Dvě milisekundy po startu byla zavolána metoda main(),

– ta v čase 0,019 zavolala metodu testMain(),

– ta začala v čase 0,021 zavádět třídu, čímž se spustil konstruktor třídy, což je metoda s interním názvem <clinit>,

– konstruktor třídy prakticky okamžitě zavolal konstruktor instance (jeho interní název je v Javě <init>, ale tady jej označuji jenom jako Constructor),

– konstruktor instance v čase 0,51 oznámil, že se chystá si dát malého šlofíka,

– čehož využije druhé vlákno a nastartuje metodu run(). V tomto druhém vlákně začíná zanořování volaných metod opět od začátku.

Takže tolik předehra a nyní k vlastnímu programu.

2. Klasicky koncipovaný jedináček

Podívejme se nejprve na klasicky koncipovaného jedináčka, který má reprezentovat aplikační okno. Následující třídu jsem vybavil komentáři, v nichž jsem se pokusil vysvětlit účel jednotlivých částí. Všechny grafické objekty, které by se měly inicializovat ve frontě událostí, zde nahrazuje atribut xxx typu Object. Kdo se chce více přiblížit aplikaci řešící GUI, může pro něj definovat např. typ JFrame a opravdu vytvářet aplikační okno.

Tady bych udělal malou odbočku pro ty, kteří vytvářejí aplikační okna typu Frame nebo JFrame. Většina tutoriálů používá ve svých ukázkách třídy, které jsou potomky některé ze jmenovaných tříd. Já to svým studentům vřele nedoporučuji, protože např. potomek třídy JFrame zdědí od svého rodiče okolo 361 metod (Java 7) z nich převážná většina je veřejných, a proto je může kdokoliv použít. O nějakém zapouzdření si pak můžeme pouze vyprávět. Proto doporučuji definovat aplikační okno jako atribut třídy a zveřejnit pouze ty metody, jejichž zveřejnění sami uznáme za vhodné.

/* The file is saved in UTF-8 codepage.
 * Check: «Stereotype», Section mark-§, Copyright-©,
Alpha-α, Beta-β, Smile-☺
 */
package awt_edt;
 
import java.awt.EventQueue;
 
 
 
public final class Tst_1
{
    /** Atribut odkazující na jedinou existující instanci
této třídy -
     *  inicializujeme jej hned v deklaraci. */
    private static final Tst_1 singleton = new Tst_1();
 
    /** Následující atribut zastupuje všechny atributy,
     *  které by se měly inicializovat v kódu zařazeném
     *  do fronty událostí. */
    private Object xxx;
 
 
    public static Tst_1 getInstance()
    {
        //Oslovím jeden z atributů jedináčka - např. aby se
zobrazil
        singleton.xxx.toString();
        return singleton;
    }
 
 
    private Tst_1()
    {
        //Při konstrukci instance je třeba provést několik
operací,
        //které by se měly správě vykonávat ve frontě
událostí.
        //Připravím si proto objekt s metodou, která vše
vykoná.
        Runnable runnable = new Runnable()
        {
            @Override
            public void run() {
                //Akci definuji pro přehlednost v separátní
metodě
                initializaton();
            }
        };
        //A zařadím objekt do fronty událostí, aby se ve
vhodnou chvíli vykonal
        EventQueue.invokeLater(runnable);
    }
 
 
    private void initializaton()
    {
        //Následující příkaz zastupuje všechny akce,
        //které je vhodné vykonávat řízeně ve frontě
událostí
        xxx  = new Object();
     }
 
 
 
    /** Třída, z níž se bude volat tovární metoda
getInstance().
     *  Metodu volám z jiné třídy, aby se jasně oddělilo
zavedení třídy
     *  od jejího použití.
     *  Třída je definována jako interní jenom proto,
     *  abych mohl mít metodu main ve stejném souboru jako
testovaný kód.
     *  Protože je třída definována jako statická, je
pak jedno,
     *  je-li uvnitř třídy a nebo někde zcela jinde.
     */
    public static class Main
    {
        public static void testMain()
        {
            Tst_1 instance = Tst_1.getInstance();
        }
        /** @param args Command line arguments - not used.
*/
        public static void main(String[] args)  {
            testMain();
        }
    }
}

2.1. Verze s kontrolními tisky

Jak už jsem řekl v úvodu, výše popsané řešení nefungovalo. Přesněji fungovalo jenom někdy a nikdy se nedalo říct dopředu, jestli se spuštění tentokrát povede. Vytvořil jsem proto kopii předchozí třídy, pojmenoval ji Tst_1A, vzal si na pomoc výše zmíněnou ladící třídu ThreadMessages a doplnil zdrojový text kontrolními tisky. Když z definice odstraníme (doufejme, že nyní již nepotřené) komentáře, obdržíme definici:

package awt_edt;
 
import java.awt.EventQueue;
 
import static dbg.ThreadMessages.*;
 
 
public final class Tst_1A
{
    static {
        msgS("<clinit> # Tst_1");
    }
 
    private static final Tst_1A singleton = new Tst_1A();
 
    private Object xxx;
 
 
    public static Tst_1A getInstance()
    {
        msgS("getInstance(): singleton=" +
singleton);
        try {
              msg("getInstance(): singleton.xxx="
+
                  singleton.xxx.toString());
        }
        catch (Exception e) {
            msg("getInstance(): crashed");
            e.printStackTrace(System.out);
        }
        msgF("getInstance(): singleton=" +
singleton);
        return singleton;
    }
 
 
    private Tst_1A()
    {
        msgS("Constructor");
        Runnable runnable = new Runnable()
        {
            @Override
            public void run() {
                    msgS("invokeLater - run()");
                    initializaton();     //### Critical
point ###
                    msgF("invokeLater - run()");
            }
        };
        EventQueue.invokeLater(runnable);
        msg ("Constructor: Start to sleep");
        pause(500);
        msgF("Constructor: this=" + this);
    }
 
 
    private void initializaton()
    {
        msgS("initializaton(): this=" + this);
        xxx  = new Object();
        msgF("initializaton(): this=" + this);
     }
 
 
    @Override
    public String toString()
    {
        return "Tst_1{" + "xxx=" + xxx
+ '}';
    }
 
 
    private static class Main
    {
        public static void testMain()
        {
            msgS("testMain()");
            Tst_1A instance = Tst_1A.getInstance();
            msgF("testMain()");
        }
        /** @param args Command line arguments - not used.
*/
        public static void main(String[] args)  {
            msgS("main() # Tst_1.Main");
            testMain();
            msgF("main() # Tst_1.Main");
        }
    }
 
    static {
        msg ("<clinit>: Before next sleep,
singleton=" + singleton);
        pause(500);
        msgF("<clinit> # Tst_1: singleton="
+ singleton);
    }
}

V předchozí definici bych vás chtěl upozornit na statické inicializační bloky na počátku a konci definice, které ohraničují konstruktor třídy, tj. kód, který se provádí při zavádění třídy do paměti a který je v každé třídě uložen v metodě s interním názvem <clinit>. Hovořím o něm proto, že jsem se už setkal s řadou profesionálních programátorů, kteří o existenci této metody neměli tušení a proces zavádění třídy považovali za jakousi podivuhodnou jimi neovlivnitelnou magii.

Kdo by chtěl do této magie trochu proniknout a dozvědět se o chování konstruktoru třídy a konstruktoru instancí trochu víc, může se podívat do knihy OOP – Naučte se myslet a programovat objektově, kde jsou dané problematice věnovány dvě kapitoly. Přestože je to kniha pro začátečníky, tak už mi několik profesionálů řeklo, že se z ní dozvěděli něco, co ještě nevěděli.

Kromě toho jsem v metodě getInstance() obalil příkaz oslovující atribut xxx blokem try … catch, aby mi vyhození výjimky při oslovení neinicializovaného atributu nezhroutilo aplikaci.

Přidal jsem také definici metody toString(), abychom mohli snadno zjišťovat interní stav testovaného objektu.

2.2. Obdržené výsledky

Předchozí třídu jsem několikrát (mnohokrát) spustil a dostal jsem tři druhy sad kontrolních tisků.

2.2.1. Nezdařilo se

Reprezentant první sady je následující:

   0,002 - Started:  main() # Tst_1.Main
   0,021 - > Started:  testMain()
   0,022 - > > Started:  <clinit> # Tst_1
   0,023 - > > > Started:  Constructor
   0,082 - > > > > Constructor: Start to sleep
   0,086 + Started:  invokeLater - run()
   0,583 - > > > Finished: Constructor:
this=Tst_1{xxx=null}
   0,583 - > > > <clinit>: Before next
sleep, singleton=Tst_1{xxx=null}
   1,084 - > > Finished: <clinit> # Tst_1:
singleton=Tst_1{xxx=null}
   1,084 - > > Started:  getInstance():
singleton=Tst_1{xxx=null}
   1,085 - > > > getInstance(): crashed
java.lang.NullPointerException
   at awt_edt.Tst_1.getInstance(Tst_1.java:30)
   at awt_edt.Tst_1$Main.testMain(Tst_1.java:79)
   at awt_edt.Tst_1$Main.main(Tst_1.java:85)
   1,085 - > > Finished: getInstance():
singleton=Tst_1{xxx=null}
   1,086 - > Finished: testMain()
   1,086 - Finished: main() # Tst_1.Main
   1,086 + > Started:  initializaton(): this=Tst_1{xxx=null}
   1,088 + > Finished: initializaton(): this=Tst_1{xxx=java.lang.Object@863399}
   1,089 + Finished: invokeLater - run()

Podívejme se, co se vlastně stalo:

Test odstartoval a hlavní vlákno běželo až do okamžiku, kdy si konstruktor „šel dát šlofíka“, aby umožnil vláknu fronty událostí vykonat kód uložený v metodě objektu runnable. V čase 0,082 s toto druhé vlákno opravdu odstartovalo, ale zmohlo se pouze na vytištění kontrolního tisku. Před zavoláním metody initializaton()se zastaví na místě označeném ve zdrojovém kódu komentářem:

//### Critical point ###

Řízení se proto po půl vteřině spánku vrací prvnímu vláknu, které ale najde oslovovaný atribut neinicializovaný a oznamuje výjimku. Na objednanou inicializaci dojde až po ukončení celého programu, kdy už je na vše pozdě.

2.2.2. Těsně vedle

Vlákno obsluhující frontu událostí se dostalo ke slovu až po skončení programu proto, že hlavní vlákno bylo tak rychlé, že stihlo vše udělat v přiděleném čase dřív, než mu virtuální stroj odebral procesor. Někdy (i když velmi zřídka) se ale stalo, že se vlákno fronty událostí dostalo ke slovu o něco dříve. Reprezentantem takovéto sady kontrolních tisků je např.:

   0,002 - Started:  Tst_1.Main.main()
   0,028 - > Started:  testMain()
   0,030 - > > Started:  <clinit> # Tst_1
   0,032 - > > > Started:  Constructor
   0,119 - > > > > Constructor: Start to sleep
   0,122 + Started:  invokeLater - run()
   0,619 - > > > Finished: Constructor:
this=Tst_1{xxx=null}
   0,619 - > > > <clinit>: Before next sleep
- singleton=Tst_1{xxx=null}
   1,120 - > > Finished: <clinit> # Tst_1:
singleton=Tst_1{xxx=null}
   1,120 - > > Started:  getInstance():
singleton=Tst_1{xxx=null}
   1,121 + > Started:  initializaton():
this=Tst_1{xxx=null}
   1,122 + > Finished: initializaton():
this=Tst_1{xxx=java.lang.Object@157f0dc}
   1,122 + Finished: invokeLater - run()
   1,122 - > > > getInstance(): crashed
java.lang.NullPointerException
   at awt_edt.Tst_1.getInstance(Tst_1.java:30)
   at awt_edt.Tst_1$Main.testMain(Tst_1.java:79)
   at awt_edt.Tst_1$Main.main(Tst_1.java:85)
   1,123 - > > Finished: getInstance():
singleton=Tst_1{xxx=java.lang.Object@157f0dc}
   1,123 - > Finished: testMain()
   1,124 - Finished: Tst_1.Main.main()

Začátek je stejný jako minule. Vlákno fronty událostí ale dostalo tentokrát druhou šanci těsně po skončení konstruktoru třídy – v čase 1,121. Problém byl ale v tom, že to už byla rozjetá metoda getInstance(), která si už nejspíš načetla prázdný odkaz v atributu xxx, takže když se hlavnímu vláknu vrátilo zpět řízení, stejně zhavarovalo, protože oslovovalo zapamatované null, aniž by vědělo o tom, že nyní už je oslovovaný atribut inicializovaný.

2.2.3. Trefa

Předchozí situace nastávala spíše výjimečně. Častěji se stávalo, že vlákno obsluhy událostí dostalo procesor dostatečně včas na to, aby stihlo inicializovat atribut xxx před tím, než si z něj metoda getInstance() bude vyzvedávat odkaz. Sada kontrolních tisků pak mohla vypadat třeba následovně:

   0,002 - Started:  main() # Tst_1.Main
   0,019 - > Started:  testMain()
   0,021 - > > Started:  <clinit> # Tst_1
   0,021 - > > > Started:  Constructor
   0,051 - > > > > Constructor: Start to sleep
   0,052 + Started:  invokeLater - run()
   0,551 - > > > Finished: Constructor:
this=Tst_1{xxx=null}
   0,551 - > > > <clinit>: Before next
sleep, singleton=Tst_1{xxx=null}
   1,052 - > > Finished: <clinit> # Tst_1:
singleton=Tst_1{xxx=null}
   1,052 + > Started:  initializaton():
this=Tst_1{xxx=null}
   1,053 + > Finished: initializaton():
this=Tst_1{xxx=java.lang.Object@157f0dc}
   1,053 + Finished: invokeLater - run()
   1,054 - > > Started:  getInstance():
singleton=Tst_1{xxx=null}
   1,054 - > > > getInstance(): singleton.xxx=java.lang.Object@157f0dc
   1,054 - > > Finished: getInstance():
singleton=Tst_1{xxx=java.lang.Object@157f0dc}
   1,055 - > Finished: testMain()
   1,055 - Finished: main() # Tst_1.Main

3. Řešení

Možných řešení předchozího problému je několik.

3.1. Publikace

První objevil Maaartin (přezdívka jednoho z účastníků konference, kde jsme tento problém probírali): stačí definovat metodu initialization() jako veřejnou (funguje to ale i když má metoda nastaven implicitní přístup označovaný jako package private). Vlákno obsluhy událostí se pak nebude zastavovat před jejím spuštěním a sada výpisů může vypadat např. následovně:

   0,002 - Started:  main() # Tst_1.Main
   0,022 - > Started:  testMain()
   0,024 - > > Started:  <clinit> # Tst_1
   0,024 - > > > Started:  Constructor
   0,060 - > > > > Constructor: Start to sleep
   0,068 + Started:  invokeLater - run()
   0,069 + > Started:  initializaton():
this=Tst_1{xxx=null}
   0,069 + > Finished: initializaton():
this=Tst_1{xxx=java.lang.Object@157f0dc}
   0,069 + Finished: invokeLater - run()
   0,561 - > > > Finished: Constructor:
this=Tst_1{xxx=java.lang.Object@157f0dc}
   0,561 - > > > <clinit>: Before next
sleep, singleton=Tst_1{xxx=java.lang.Object@157f0dc}
   1,062 - > > Finished: <clinit> # Tst_1:
singleton=Tst_1{xxx=java.lang.Object@157f0dc}
   1,063 - > > Started:  getInstance():
singleton=Tst_1{xxx=java.lang.Object@157f0dc}
   1,063 - > > > getInstance():
singleton.xxx=java.lang.Object@157f0dc
   1,063 - > > Finished: getInstance():
singleton=Tst_1{xxx=java.lang.Object@157f0dc}
   1,064 - > Finished: testMain()
   1,064 - Finished: main() # Tst_1.Main

Nevýhodou tohoto řešení je ale to, že zveřejníme kód, který se přehrabuje v útrobách našeho objektu a do kterého nikomu nic není. Pak stačí zapomenout, proč jsme jej zveřejňovali, použít jej omylem na nevhodném místě a už můžeme začít bádat nad tím, proč náš skvělý program najednou přestal chodit nebo se chová naprosto podivně.

3.2. Odložená (líná) inicializace

Druhým řešením je odložená inicializace jedináčka (správný překlad je odložená inicializace, ale mně se doslovný překlad termínu lazy initialization = lína inicializace líbí víc).

Správný postup jsem již publikoval v knize Návrhové vzory – 33 vzorových postupů pro objektové programování, ale pro jistotu jej tu zopakuji. Při tomto postupu se atribut odkazující na jedináčka neinicializuje v deklaraci, ale až při prvním volání metody getInstance() nebo jejího ekvivalentu. Většinou tím nic nezískáme, spíš ztratíme, ale zrovna v našem případě tak dosáhneme toho, že inicializace bude probíhat až po dokončení konstruktoru třídy, takže nebudou problémy s blokací vlákna obsluhy fronty událostí.

V nově koncipované třídě budou dvě změny. Za prvé se nám změní definice metody getInstance(). Nově definovaná metoda se nejprve podívá, jestli byl již jedináček vytvořen. Pokud ne, otevře kritickou oblast a znovu se zeptá (nebudu vysvětlovat, proč to dělá – podrobnosti najdete ve výše zmíněné knize). Když si je 100% jistá, že instanci opravdu nikdo nevytvořil, tak ji vytvoří a inicializuje příslušný atribut. Pak už se chová stejně, jako kdyby už jedináček dávno vytvořen byl.

public static Tst_2 getInstance()
{
    if (singleton == null) {
        synchronized(Tst_2.class) {
            if (singleton == null) {
                singleton = new
Tst_2();
            }
        }
    }
    singleton.xxx.toString();
    return singleton;
}

Druhá změna se odehrála v konstruktoru. Opět se sice předává inicializace do vlákna obsluhy fronty událostí, avšak tentokrát prostřednictvím metody invokeAndWait(), která vrátí řízení volající metodě až poté, co bude svěřený kód vykonán. (Musíme ale myslet na to, že metoda může vyhodit kontrolovanou výjimku, kterou musíme ošetřit.) Když proto konstruktor končí, tak už ví, že všechny jeho atributy jsou správně inicializovány.

private Tst_2()
{
    try {
        Runnable runnable = new Runnable()
        {
            @Override
            public void run() {
                initializaton();
            }
        };
        EventQueue.invokeAndWait(runnable);
    }
    catch (Exception ex) {
        throw new RuntimeException(
                  "nCreation of an application window
crashed", ex );
    }
}

Možná se zeptáte, proč jsme čekací metodu nemohli použít i v minulé verzi. Tam to nešlo proto, že by metoda čekala na dokončení provedení svěřeného kódu, ale jeho provedení by čekala na dokončení konstruktoru třídy, takže by se nám celá aplikace kousla. Takto jsme měli alespoň v některých případech šanci, že vlákno obsluhy fronty událostí stihne dokončit svoji práci včas.

Tentokrát však není konstruktor jedináčka volán z konstruktoru jeho třídy, ale z metody getInstance(), které je volána z prostoru mimo třídu až poté, co je celá třída zavedena a její konstruktor ukončen. Nic tedy provedení metody ve vlákně obsluhy fronty událostí nebrání a má proto smysl si na dokončení tohoto kódu počkat.

Sada kontrolních tisků takto upravené třídy by mohla vypadat např. následovně:

   0,030 - > Started:  testMain()
   0,032 - > > Started:  <clinit> # Tst_2A
   0,032 - > > Finished: <clinit> # Tst_2A:
singleton=null
   0,032 - > > Started:  getInstance():
singleton=null
   0,033 - > > > getInstance(): call constructor
   0,033 - > > > Started:  Constructor
   0,065 + Started:  invokeAndWait - run()
   0,066 + > Started:  initializaton():
this=CM_3_nonAWT{xxx=null}
   0,066 + > Finished: initializaton():
this=CM_3_nonAWT{xxx=java.lang.Object@157f0dc}
   0,066 + Finished: invokeAndWait - run()
   0,067 - > > > Finished: Constructor:
this=CM_3_nonAWT{xxx=java.lang.Object@157f0dc}
   0,067 - > > > getInstance():
singleton=CM_3_nonAWT{xxx=java.lang.Object@157f0dc}
   0,067 - > > Finished: getInstance():
singleton=CM_3_nonAWT{xxx=java.lang.Object@157f0dc}
   0,068 - > Finished: testMain()
   0,068 - Finished: main() # Tst_2A.Main

Jak se můžete přesvědčit, konstruktor tentokrát končí s inicializovanými atributy a program proto může bez obav pokračovat.

Pro zájemce ještě přidám definici upravené třídy (verzi s doplněnými kontrolními tisky najdete v přiložených souborech):

/*
 
 Check: «Stereotype», Section mark-§, Copyright-©, Alpha-α, Beta-β,
Smile-☺
 */
package awt_edt;
 
import java.awt.EventQueue;
import javax.swing.JFrame;
 
 
public final class Tst_2
{
   /** This field cannot be final. */
   private static Tst_2 singleton;
 
   private volatile Object xxx;
 
 
   public static Tst_2 getInstance()
   {
       if (singleton == null) {
           synchronized(Tst_2.class) {
               if (singleton == null) {
                   singleton = new Tst_2();
    
           }
           }
       }
       singleton.xxx.toString();
       return singleton;
   }
 
 
   private Tst_2()
   {
       try {
           Runnable runnable = new Runnable()
           {
               @Override
               public void run() {
                   initializaton();
               }
           };
           EventQueue.invokeAndWait(runnable);
       }
       catch (Exception ex) {
           throw new RuntimeException(
                     "nCreation of an application window crashed", ex );
       }
   }
 
 
   private void initializaton()
   {
       xxx  = new Object();
   }
 
 
   public static class Main
   {
       public static void testMain()
       
           Tst_2 instance = Tst_2.getInstance();
       }
       /** @param args Command line arguments - not used. */
       public static void main(String[] args)  {
           testMain();
       }
   }
}

4. Problém je obecnější

Nějakou dobu jsem si myslel, že se jedná pouze o zvláštní chování obsluhy fronty událostí. Když jsem se totiž o totéž pokusil prostřednictvím klasicky konstruovaných vláken, tak vše chodilo. Když jsem ale měl už hotový tento článek, tak jsem si všiml, že to chodilo pouze proto, že jsem zapomněl definovat vyvolávanou metodu jako soukromou. Zkuste definovat konstruktor první verze následovně (ukazuji verzi s kontrolními tisky nazvanou Test_1B):

private Tst_1B()
{
    msgS("Constructor");
    Runnable runnable = new Runnable()
    {
        @Override
        public void run() {
            msgS("Thread_2 - run()");
            initializaton();     //### Critical point ###
            msgF("Thread_2 - run()");
        }
    };
    new Thread(runnable, "Thread_2").start();
    msg ("Constructor: Start to sleep");
    pause(500);
    msgF("Constructor: this=" + this);
}

Zjistíte, že se chová naprosto stejně, jako když jsme pro spuštění inicializace používali metodu EventQueue.in­vokeLater.

Opět mám k dispozici dvě řešení: buď u metody initializaton() odstranit modifikátor private, anebo použít odloženou inicializaci a konstruktor pak upravit do tvaru (opět uvádím podobu s kontrolními tisky):

private Tst_2B()
{
    msgS("Constructor");
    Runnable runnable = new Runnable()
    {
        @Override
        public void run() {
            msgS("invokeAndWait - run()");
            initializaton();
            msgF("invokeAndWait - run()");
        }
    };
    Thread thread2 = new Thread(runnable,
"Thread_2");
    thread2.start();
    try {
        thread2.join();
    } catch (InterruptedException ex) {
        //Ošetření přerušení
    }
    msgF("Constructor: this=" + this);
}

Zde se na druhé vlákno čeká zavoláním jeho metody join(). Tak, jak je to napsané, to vypadá nesmyslně – proč startovat druhé vlákno, abych pak na něj vzápětí čekal? V běžném programu ale můžete mezi odstartování vlákna a čekání na jeho ukončení vložit řadu dalších činností. Berte to proto opravdu pouze jako demonstrativní ukázku.

5. Závěr

V článku jsem se pokusil ukázat, že program, jehož činnost je rozprostřena do několika vláken, se v některých situacích může chovat zdánlivě podivně. Někdy za to může naše nešikovnost při jeho konstrukci, jindy neznalost pravidel fungování používaných knihoven. V tomto případě jsme si ukazovali, že vlákno odmítá spouštět soukromé metody třídy, která byla zaváděna v jiném vlákně, do chvíle, než je třída zcela zavedena, tj. než konstruktor třídy <clinit> ukončí svoji činnost. Na nesoukromé metody taková omezení kladena nejsou.

Přiznám se, že stále nevím, jestli popisované chování je chybou, anebo se jedná o záměr tvůrců knihovny. Jestli je to ale záměr, tak by jej měli někde zdokumentovat. Standardní příručky však o této vlastnosti obsluhy vlákna fronty událostí mlčí. S uvedeným chováním jsem se poprvé setkal v Javě 6 (předtím se mi daný program nekousal, ale nevím, jestli jsem jenom neměl štěstí), ale vyzkoušel jsem, že zcela stejně se chová i nově vydaná Java 7.

K článku si můžete stáhnout ukázkové zdrojové kódy

Komentáře

Subscribe
Upozornit na
guest
62 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
problem

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

rudyment

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ě.

Mips

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.

rudyment

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í.

Karel

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.

rudyment

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.

espinosa_cz

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

Sid

Bod 4) Nestartovat thread z konstruktoru!

rudyment

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í.

Filip Jirsák

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.

rudyment

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.

Filip Jirsák

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.

rudyment

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

Jirka P

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čí?

Filip Jirsák

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.

rudyment

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.

Martin Malý

Redakce se omlouvá autorovi i čtenářům, zdrojové kódy byly doplněny.

x22

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>.)

rudyment

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.

Leoš

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

Leoš

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.

Filip Jirsák

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.

Leoš

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()?

rudyment

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.

Filip Jirsák

Ř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.

rudyment

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.

Leoš

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.

Leoš

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“.

KarelI

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

thingol

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.

Filip Jirsák

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.

Maaartin

> 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).

Filip Jirsák

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).

v6ak

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.

Filip Jirsák

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.

v6ak

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.

Maaartin

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.

Filip Jirsák

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.

Filip Jirsák

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.

Jirka P

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.

René Stein

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ě.

Petr Lazecky

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…

Rene Stein

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í.

Heron

<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.

Leoš

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“. ;-)

rudyment

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.

Filip Jirsák

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í.

Leoš

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.

Aminux

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.

rudyment

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“

kink

V kapitole 17 se opravdu doctete jak funguje pametovy model, vas kod odpovida prikladu v bode „17.11 Example: Out-of-Order Writes“, nicmene doporucuji si precist clanek http://www.ibm.com/developerworks/java/library/j-dcl/index.html , kde je problem rozebran podrobneji.

rudyment

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.

kink

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.

flv

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

Muzete to rozvest ?

kink

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.

X

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.

Bjarne

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.

rudyment

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.

Bjarne

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.

Viktor

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. ?

Bjarne

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 ;)

marvertin

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

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.