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.