V tomto článku se podíváme na data race. Data race je synchronizační chyba, která se objevuje ve vícevláknových programech. Řekneme si, kdy tato chyba nastává, ukážeme si pár příkladů a představíme si nástroj, kterým lze data race detekovat.

Pokud dvě vlákna přistupují ke sdílené proměnné, alespoň jedno vlákno zapisuje a mezi přístupy není žádné uspořádání pomocí relace happens-before, nastává data race. Chybějící uspořádání může způsobit velmi překvapivé chování programu. Např. pokud a, b mají hodnotu 0 a vlákno 1 vykoná

a = 1;
x = b;
System.out.println("x = " + x);

a vlákno 2 vykoná

b = 2;
y = a;
System.out.println("y = " + y);

je zřejmé, že na výstupu můžeme dostat (v libovolném pořadí) x = 0 a y = 1 (vlákno 1 provedlo přiřazení dříve než vlákno 2), x = 2 a y = 0 (vlákno 2 provedlo přiřazení dříve než vlákno 1) nebo x = 2 a y = 1 (vlákno 1 provedlo a = 1 a pak vlákno 2 provedlo b = 2). Překvapivě ovšem můžeme dostat také x = 0 a y = 0, protože chybějící uspořádání při přístupu k proměnné a může způsobit, že vlákno 2 nevidí změnu, kterou provedlo vlákno 1 (a analogicky vlákno 1 nemusí vidět změnu, kterou provedlo vlákno 2 na proměnné b).

Máme-li v programu data race, jde obvykle o chybu, která je obtížně detekovatelná, protože se může projevit jen někdy (např. pouze na některých architekturách). Podívejme se na příklad. V následujícím kódu chybí uspořádání při přístupu k proměnné x.

public class Increment implements Runnable {
    int x;
    @Override
    public void run() {
        x++;
    }
}

public class Test1 {
    public static void main(String[] args) {
        Runnable r = new Increment();
        new Thread(r).start();
        new Thread(r).start();
    }
}

Chceme-li se chybě data race vyhnout, je třeba zajistit, aby mezi každými dvěma přístupy ke sdílené proměnné byla relace happens-before. Tuto relaci lze v programu zavést několika způsoby. Např. pokud vlákno t1 spustí vlákno t2, pak vše, co se vykonalo v t1 před zavoláním t2.start(), je v relaci happens-before s tím, co proběhne v t2. Jiný způsob, jak můžeme relaci zavést, je použití klíčového slova synchronized nebo tříd z balíku java.util.concurrent. Podívejme se na příklady. V následujícím kódu je uspořádání při přístupu k proměnné x definováno pomocí metody start() třídy java.lang.Thread.

public class Test2 {
    public static void main(String[] args) {
        Increment p = new Increment();
        p.x = 1;
        new Thread(p).start();
    }
}

V dalším příkladu je uspořádání definováno pomocí monitoru. Všimněte si, že i když je tento program správně synchronizovaný, není určeno, v jakém pořadí vlákna vykonají synchronizované sekce.

public class Decrement implements Runnable {
    int x;
    @Override
    public void run() {
        synchronized (this) {
            x--;
        }
    }
}

public class Test3 {
    public static void main(String[] args) {
        Runnable r = new Decrement();
        new Thread(r).start();
        new Thread(r).start();
    }
}

Dále se podíváme na možnosti detekce této chyby. Algoritmy pro detekci lze rozdělit na statické a dynamické. Statické algoritmy hledají chyby zkoumáním zdrojového nebo bajtového kódu. Dynamické algoritmy sbírají data za běhu programu a tato data vyhodnocují. Mohou sledovat buď množiny zámků (monitorů) nebo relaci happens-before. Algoritmy, které sledují množiny zámků, jsou založeny na předpokladu, že ve správně synchronizovaném programu je přístup ke sdílené proměnné strážen nějakým zámkem (monitorem). Detekce chybějící synchronizace probíhá takto: při každém přístupu ke sdílené proměnné zjistíme aktuální množinu zámků a průnik této množiny s množinami zámků z předchozích přístupů. Pokud je průnik prázdný, ohlásíme data race. Výhodou tohoto přístupu je snadná implementace, nevýhodou je poměrně velké množství falešných hlášení. Algoritmy, které sledující relaci happens-before, monitorují akce, jež tuto relaci vytvářejí. Např. vstup do synchronizované sekce, volání metody start() nebo návrat z metody join() na objektu typu java.lang.Thread nebo metody lock() a unlock() na objektu typu java.util.concurrent.locks.Lock. Pokud detekují dva přístupy bez relace happens-before, ohlásí data race. Tyto algoritmy dávají přesnější výsledky než algoritmy založené na množinách zámků, jsou však náročnější na implementaci.

K detekci data race v programu lze použít projekt JaDaRD (Java Data-Race Detector). JaDaRD je tzv. JVM agent, což je dynamická knihovna, kterou JVM přilinkuje při spuštění. Za běhu programu JaDaRD monitoruje přístupy ke sdíleným proměnným a sleduje zámky používané při těchto přístupech. Umí sledovat také metody start() a join() na objektech java.lang.Thread (přepínač -watchThreads) a metody lock() a unlock() na objektech java.util.concurrent.locks.Lock (přepínač -watchConcurrent). Agenta spustíme pomocí argumentů na příkazové řádce. Pro detekci data race v balíku simple můžeme použít např.

java -agentpath:jadard.dll=-stackTrace-trie-watchThreads-loggerLevel=WARN-package=simple simple.Test1

Argument -stackTrace způsobí výpis obsahu zásobníku při nalezeném data race a -trie zapíná efektivní ukládání informací do TRIE. Více na wiki projektu.