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.

Č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

Evangelista objektově orientovaného programování a jeho prosazení do výuky. Pracuji jako Senior EDU expert ve firmě ICZ a vedle toho učím programování na VŠE.

Komentáře: 62

Přehled komentářů

problem a kde je problem?
rudyment Re: a kde je problem?
Mips Re: a kde je problem?
rudyment Re: a kde je problem?
Karel Re: a kde je problem?
rudyment Re: a kde je problem?
espinosa_cz 3 rady od boku
Sid Re: 3 rady od boku
rudyment Re: 3 rady od boku
Filip Jirsák Re: 3 rady od boku
rudyment Re: 3 rady od boku
Filip Jirsák Re: 3 rady od boku
rudyment Re: 3 rady od boku
Jirka P Re: 3 rady od boku
Filip Jirsák Re: 3 rady od boku
rudyment Re: 3 rady od boku
Martin Malý Re: 3 rady od boku
x22 private vs. public
rudyment Re: private vs. public
Leoš Špatně udělaná líná inicializace
Leoš Re: Špatně udělaná líná inicializace
Filip Jirsák Re: Špatně udělaná líná inicializace
Leoš Re: Špatně udělaná líná inicializace
rudyment Re: Špatně udělaná líná inicializace
Filip Jirsák Re: Špatně udělaná líná inicializace
rudyment Re: Špatně udělaná líná inicializace
Leoš Re: Špatně udělaná líná inicializace
Leoš Re: Špatně udělaná líná inicializace
ijacek Re: Špatně udělaná líná inicializace
thingol Re: Špatně udělaná líná inicializace
Filip Jirsák konstrukce objektu není atomická operace
Maaartin Re: konstrukce objektu není atomická operace
Filip Jirsák Re: konstrukce objektu není atomická operace
v6ak Re: konstrukce objektu není atomická operace
Filip Jirsák Re: konstrukce objektu není atomická operace
v6ak Re: konstrukce objektu není atomická operace
Maaartin Re: konstrukce objektu není atomická operace
Filip Jirsák Re: konstrukce objektu není atomická operace
Filip Jirsák Re: konstrukce objektu není atomická operace
Jirka P Re: konstrukce objektu není atomická operace
René Stein Klasický deadlock?
Petr Lazecky Re: Klasický deadlock?
Rene Stein Re: Klasický deadlock?
Heron Re: Klasický deadlock?
Leoš 1 rada od boku
rudyment Re: 1 rada od boku
Filip Jirsák Re: 1 rada od boku
Leoš Re: 1 rada od boku
Aminux Re: 1 rada od boku
rudyment Re: 1 rada od boku
kink Re: 1 rada od boku
rudyment Re: 1 rada od boku
kink Re: 1 rada od boku
flv Re: 1 rada od boku
kink Re: 1 rada od boku
X Re: 1 rada od boku
Bjarne Chyba v článku
rudyment Re: Chyba v článku
Bjarne Šotek číslo 2?
Viktor AWT???
Bjarne Re: AWT???
marvertin Konstruktor konstruuje objekt
Zdroj: https://www.zdrojak.cz/?p=3525