concurrency-threads.html 23.7 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
<h4>Parallelität und Nebenläufigkeit</h4>

<p>Die parallele Verarbeitung von Prozessen setzt voraus, dass mehrere Prozessoren (oder Prozessorkerne) zur Verfügung stehen, da jeder Prozessor nur einen Prozess gleichzeitig ausführen kann. Ob die Prozessoren in einem einzigen Computer verbaut sind oder sich auf mehrere Computer verteilen, ist dabei nicht relevant. In der Anwendungsentwicklung hat sich zusätzlich der Begriff Nebenläufigkeit (engl. <i>Concurrency</i>) etabliert, der potentielle Parallelität beschreibt. Während der Anwendungsentwicklung ist häufig nicht bekannt, auf welcher Hardware die Komponenten der Anwendung später ausgeführt werden. Das umfasst auch die Kenntnis darüber, wie viele Prozessoren tatsächlich zur Verfügung stehen. Demzufolge wird eine Anwendung i.d.R. nur auf potentielle Parallelität ausgelegt. Die Anwendung könnte auch auf einem Computer mit nur einem Prozessor ausgeführt werden. Dieser Prozessor wird dann mehrere Prozesse verzahnt ausführen. Das Betriebssystem steuert die Verschränkung (engl. <i>Interleaving</i>) der nebenläufigen Prozesse. Prozesse mit höherer Priorität bekommen mehr Prozessorzeit zugewiesen. Auch in dem verbreiteten Lehrbuch "Principles of Concurrent and Distributed Programming" von M. Ben-Ari werden die Begriffe Parallelität und Nebenläufigkeit wie folgt abgegrenzt <a href="#cite-Ben05">[Ben05]</a>.</p>

<div class="cite">"<u>Parallel System</u>: A system in which the execution of several programs overlap in time by running on separate processors.<br>
<u>Concurrent Program</u>: A concurrent program is a set of processes that can be executed in parallel. Thus, a concurrent program does not necessarily overlap its computation, but may do so." (M. Ben-Ari)</div>

<img src="media/concurrency_parallel.png" style="width:450px">
<label>Parellelität und Nebenläufigkeit von Prozessen</label>

<h4>Prozesse und Threads</h4>

<p>Ein Prozess bezeichnet allgemein den gerichteten Ablauf eines Geschehens. In der Softwareentwicklung ist das ablaufende Geschehen die Ausführung eines programmierten Kontrollflusses. Aus Sicht des Betriebssystems ist ein Prozess ein Container für mehrere zusammengehörige <i>Threads</i> einer Anwendung. Ein Thread ist ein einzelner nebenläufiger Ausführungsfaden innerhalb des Prozesses einer Anwendung. Threads sind demzufolge leichtgewichtige Teilprozesse und effizienter in der Erzeugung. Das Betriebssystem führt jeden Prozess unter einem bestimmten Betriebssystem-User mit ggf. eingeschränkten Rechten aus.</p>

<p>Beim Start eines Prozesses weist das Betriebssystem ihm einen Adressraum (= Teil des Hauptspeichers) zu, auf dem der Prozess exklusiv arbeiten kann. Die Threads innerhalb eines Prozesses teilen sich gemeinsam diesen Adressraum. Eine Java-Anwendung wird in einer <i>Java Virtual Machine (JVM)</i> als Laufzeitumgebung ausgeführt, die den Prozess aus Sicht des Betriebssystems darstellt. Der JVM wird ein begrenzter Adressraum für alle Objekte, die über Konstruktoraufrufe erzeugt werden, zur Verfügung gestellt. Diesen Adressraum bezeichnen wir in Java als <i>Heap</i>. Falls kein Heap-Speicher mehr für neue Objekte allokiert werden kann, wird ein <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/OutOfMemoryError.html"><code>OutOfMemoryError</code></a> von der JVM geworfen. In Java wird Heap-Speicher, der durch Objekte belegt ist, die nicht mehr über Referenzen aus dem Kontrollfluss erreicht werden können, automatisch durch einen <i>Garbage Collection Thread</i> freigegeben. Während sich die Threads einer Anwendung also den Heap teilen, hat jeder Thread einen eigenen <i>Stack</i>, um seinen individuellen Kontrollfluss zu verwalten. Die folgende Abbildung soll diese Beziehung zwischen mehreren Threads und ihrem gemeinsamen Heap in einer JVM verdeutlichen.</p>

<img src="media/concurrency_jvm.png" style="width:640px">
<label>Heap und Stack in einer JVM</label>

<h4>Ausführen nebenläufiger Threads</h4>
<p>In Java kann jede Klasse, die das funktionale Interface <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Runnable.html"><code>java.lang.Runnable</code></a> implementiert als nebenläufiger Thread ausgeführt werden. Der Kontrollfluss des Threads beginnt in der Methode <code>run</code> des <code>Runnable</code> (Zeile 4-7 im folgenden Code-Beispiel). Um ein <code>Runnable</code> zu starten, kann ein <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Thread.html"><code>Thread</code></a>-Objekt erzeugt werden, dem das <code>Runnable</code> übergeben wird (Zeile 10). Über die Methode <code>start</code> kann der Thread gestartet werden (Zeile 11). Dadurch wird die Methode <code>run</code> implizit nebenläufig ausgeführt. Später werden wir weitere Möglichkeiten kennenlernen, um ein <code>Runnable</code> auszuführen.</p>

<pre><code class="language-java line-numbers">class MyRunnable implements Runnable {

    @Override
    public void run() {
        String message = "Hello from " + Thread.currentThread().getName(); // output ist "Hello from Thread-0"
        System.out.println(message);
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        System.out.println("Hello from " + Thread.currentThread().getName()); // output ist "Hello from main"
    }
}</code></pre>

<p>Die Klasse <code>Thread</code> implementiert selbst das Interface <code>Runnable</code>. Es könnte daher auch einfach die Klasse <code>Thread</code> wie folgt erweitert werden, wenn dadurch die fachliche Vererbungshierarchie nicht gestört wird.</p>
<pre><code class="language-java line-numbers">class MyThread extends Thread {

    @Override
    public void run() {
        String message = "Hello from " + getName(); // output ist "Hello from Thread-0"
        System.out.println(message);
    }

    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
        System.out.println("Hello from " + Thread.currentThread().getName()); // output ist "Hello from main"
    }
}</code></pre>

<p>Da <code>Runnable</code> ein funktionales Interface ist, kann es auch in einem Lambda-Ausdruck verwendet werden.</p>
<pre><code class="language-java line-numbers">Runnable runnable = () -> System.out.println("Hello from concurrent thread");
Thread thread = new Thread(runnable);
thread.start();</code></pre>

<p>Im folgenden Code-Beispiel werden in der Klasse <code>MyLoopThread</code> 3 Threads gestartet, die jeweils die Zahlen von 1 bis 100 hochzählen und ausgeben. Anhand der Ausgaben lässt sich das zufällige <i>Thread-Interleaving</i> beobachten. Es ist nicht vorhersehbar, welcher Thread zuerst fertig wird. In der Klasse <code>MyLoopSleepThread</code> wird das voherige Beispiel nur darum erweitert, dass jeder Thread nach der Ausgabe einer Zahl für 10 ms wartet (Zeilen 9-11). Durch diese bewusste Verzögerung zählen zwar alle Threads mit hoher Wahrscheinlichkeit im Takt, aber nicht garantiert. Die Korrektheit einer Anwendung sollte nie von einem derartigen Aufruf der Methode <code>sleep</code> abhängen.</p>

<ul class="nav nav-tabs" id="threads-tabs" role="tablist">
  <li class="nav-item"><a href="#threads-tabs-loop" class="nav-link active" data-toggle="tab" role="tab">MyLoopThread</a></li>
  <li class="nav-item"><a href="#threads-tabs-loop2" class="nav-link" data-toggle="tab" role="tab">MyLoopSleepThread</code></a></li>
</ul>
<div class="tab-content" id="threads-tabs-content">
  <div class="tab-pane show active" id="threads-tabs-loop" role="tabpanel">
	<pre><code class="language-java line-numbers">class MyLoopThread extends Thread {

    MyLoopThread(String name) { super(name); }

    @Override
    public void run() {
        IntStream.range(0, 100).forEach(i -> System.out.println(getName() + "\t" + i));
    }

    public static void main(String[] args) {
        for (int i = 0; i &lt; 3; i++) {
            Thread thread = new MyLoopThread("Loop" + i);
            thread.start();
        }
    }
}</code></pre></div>
  <div class="tab-pane" id="threads-tabs-loop2" role="tabpanel">
	<pre><code class="language-java line-numbers">class MyLoopSleepThread extends Thread {

    MyLoopSleepThread(String name) { super(name); }

    @Override
    public void run() {
        IntStream.range(0, 100).forEach(i -> {
            System.out.println(getName() + "\t" + i);
            try {
                sleep(10); // sleep for 10 ms
            } catch (InterruptedException e) {}
        });
    }

    public static void main(String[] args) {
        for (int i = 0; i &lt; 3; i++) {
            Thread thread = new MyLoopSleepThread("Loop" + i);
            thread.start();
        }
    }
}</code></pre></div>
</div>

<p>Ein <code>Thread</code> wird beendet, wenn er das Ende der Methode <code>run</code> des ausgeführten <code>Runnable</code> erreicht. Es ist durchaus üblich, dass Threads dauerhaft auf bestimmte Ereignisse warten und diese verarbeiten, bis eine Abbruchbedingung erfüllt wird. Das erste der beiden folgenden Code-Beispiele zeigt, wie ein derartiges sanftes, kontrolliertes Beenden eines Threads am Ende einer Iteration herbeigeführt werden kann. Das zweite Code-Beispiel zeigt dagegen, dass ein Thread auch abrupt mit sofortiger Wirkung von außen unterbrochen werden kann, indem die Methode <code>interrupt</code> auf dem <code>Thread</code>-Objekt ausgeführt wird. Die aktuelle Iteration wird hierbei nicht mehr vollständig ausgeführt, sondern unmittelbar abgebrochen. Methoden wie z.B. <code>sleep</code>, die einen Thread in einen wartenden Zustand versetzen, werfen i.d.R. eine <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/InterruptedException.html"><code>InterruptedException</code></a>, falls sie von einem anderen Thread von außen durch Aufruf der Methode <code>interrupt</code> unterbrochen worden sind.</p>

<ul class="nav nav-tabs" id="threads2-tabs" role="tablist">
  <li class="nav-item"><a href="#threads2-tabs-stop" class="nav-link active" data-toggle="tab" role="tab">Thread über Bedingung beenden</a></li>
  <li class="nav-item"><a href="#threads2-tabs-interrupt" class="nav-link" data-toggle="tab" role="tab">Thread-Interruption</code></a></li>
</ul>
<div class="tab-content" id="threads2-tabs-content">
  <div class="tab-pane show active" id="threads2-tabs-stop" role="tabpanel">
	<pre><code class="language-java line-numbers">class MyCancelledThread extends Thread {
	
	private boolean keepRunning = true;

	public void cancel() {
		this.keepRunning = false;
	}

	@Override
	public void run() {
		while (keepRunning) {
			// do something meaningful
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		MyCancelledThread thread = new MyCancelledThread();
		thread.start();
		
		// after a while
		thread.cancel(); // call custom method to cancel the thread
	}
}</code></pre></div>
  <div class="tab-pane" id="threads2-tabs-interrupt" role="tabpanel">
	<pre><code class="language-java line-numbers">class MyInterruptedThread extends Thread {

    @Override
    public void run() {
        while (!isInterrupted()) {
            // do something meaningful
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyInterruptedThread thread = new MyInterruptedThread();
        thread.start();
        
		// after a while
        thread.interrupt(); // thread interruption
    }
}</code></pre></div>
</div>

<p>In Java können Threads eine Priorität zwischen 1 und 10 zugewiesen bekommen. Einem Thread wird je nach Priorität mehr oder weniger CPU-Zeit zugestanden. Das folgende Code-Beispiel führt 10 Threads mit jeweils unterschiedlicher Priorität aus (Zeilen 14-18). Jeder Thread belastet die CPU, indem er eine Variable inkrementiert (Zeilen 7-8). Nach 10 s werden alle Threads abgebrochen (Zeilen 20+33). Die Ausgabe in den Zeilen 23-31 zeigt exemplarisch, wie viel CPU-Zeit den Threads zugestanden worden ist. Bei jeder Ausführung wird die Ausgabe sich leicht unterscheiden. Es ist zu berücksichtigen, dass auf dem Betriebssystem weitere Prozesse ausgeführt werden, so dass die theoretisch verfügbar CPU-Zeit nicht allein auf die Java-Threads verteilt werden kann. Die Ausgabe könnte auf einem Computer mit 4 Prozessorkernen wie folgt aussehen:</p>

<pre>
CPU time of Thread-0: 187 ms
CPU time of Thread-1: 296 ms
CPU time of Thread-2: 750 ms
CPU time of Thread-3: 1078 ms
CPU time of Thread-4: 2203 ms
CPU time of Thread-5: 2593 ms
CPU time of Thread-6: 4875 ms
CPU time of Thread-7: 5046 ms
CPU time of Thread-8: 6765 ms
CPU time of Thread-9: 6859 ms
Total CPU time: 30652 ms
Available processors: 4
</pre>

<pre><code class="language-java line-numbers">class MyPriorityThread extends Thread {

    MyPriorityThread(int priority) { setPriority(priority); }

    @Override
    public void run() {
        BigInteger i = BigInteger.ZERO;
        while (!isInterrupted()) i = i.add(BigInteger.ONE); // do something to utilize CPU
    }

    public static void main(String[] args) throws InterruptedException {

        // create threads with different priorities
        List&lt;Thread> threads = new ArrayList<>();
        for (int i = 1; i &lt;= 10; i++) {
            threads.add(new MyPriorityThread(i));
        }
        threads.forEach(t -> t.start()); // start all threads
        
		sleep(10_000); // threads utilize CPU for 10 s

        // query CPU time for each thread
        ThreadMXBean tmxb = ManagementFactory.getThreadMXBean();
        long totalCpuTime = 0;
        for (Thread t : threads) {
            long cpuTime = tmxb.getThreadCpuTime(t.getId()) / 1_000_000;
            System.out.println("CPU time of " + t.getName() + ": " + cpuTime + " ms");
            totalCpuTime += cpuTime;
        }
        System.out.println("Total CPU time: " + totalCpuTime + " ms");
        System.out.println("Available processors: " + Runtime.getRuntime().availableProcessors());

        threads.forEach(t -> t.interrupt()); // interrupt threads
    }
}</code></pre>

<h4>Scheduling von Threads</h4>

<p>Es ergibt sich häufig die Anforderung, dass ein Runnable nicht sofort sondern zu einem bestimmten Zeitpunkt in der Zukunft ausgeführt werden soll, oder dass ein Runnable periodisch in bestimmten Intervallen wiederholt werden soll. Eine derartige zeitliche Ablaufsteuerung von Prozessen bezeichnen wir allgemein als <i>Scheduling</i>. Im Folgenden werden zwei Varianten für das Scheduling von Runnables in Java gezeigt. In beiden Code-Beispielen entsteht bei Ausführung die folgende Ausgabe:</p>
<pre>Timed event 1	12:01:02.689008600
Timed event 2	12:01:03.673393100
Timed event 3	12:01:04.673484800
Timed event 4	12:01:05.673454700
Timed event 5	12:01:06.673461800</pre>

<ul class="nav nav-tabs" id="threads3-tabs" role="tablist">
  <li class="nav-item"><a href="#threads3-tabs-timer" class="nav-link active" data-toggle="tab" role="tab">TimerTask</a></li>
  <li class="nav-item"><a href="#threads3-tabs-executor" class="nav-link" data-toggle="tab" role="tab">ScheduledExecutorService</code></a></li>
</ul>
<div class="tab-content" id="threads3-tabs-content">
  <div class="tab-pane show active" id="threads3-tabs-timer" role="tabpanel">
	<pre><code class="language-java line-numbers">import java.util.Timer;
import java.util.TimerTask;

class MyTimerTask extends TimerTask {

    int i = 0;

    @Override
    public void run() {
        System.out.println("Timed event " + (++i) + "\t" + LocalTime.now());
    }

    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new MyTimerTask(), 500, 1_000); // schedule timed task

        Thread.sleep(5_000);
        timer.cancel(); // cancel timer after 5 s
    }
}</code></pre></div>
  <div class="tab-pane" id="threads3-tabs-executor" role="tabpanel">
	<pre><code class="language-java line-numbers">import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
// ...

class MyScheduledExecutorService {

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger i = new AtomicInteger(0);
        Runnable runnable = () -> System.out.println("Timed event " + (i.incrementAndGet()) + "\t" + LocalTime.now());

        ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
        scheduler.scheduleAtFixedRate(runnable, 500, 1_000, TimeUnit.MILLISECONDS); // schedule timed task

        Thread.sleep(5_000);
        scheduler.shutdown(); // cancel timer after 5 s
    }
}</code></pre></div>
</div>

<ul>
<li><b>Timer</b>: In Zeile 15 wird ein <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Timer.html"><code>Timer</code></a> erzeugt, über den mittels der Methode <code>scheduleAtFixedRate</code> ein <code>TimerTask</code>-Objekt, also eine zeitgesteuerte Aufgabe, wiederholt ausgeführt werden kann (Zeile 16). Das zweite Input-Argument gibt die Verzögerung (in ms) der ersten Ausführung an, das dritte Input-Argument das Intervall (in ms) für die wiederholte Ausführung. Die Ausführung endet erst mit dem Abbruch des <code>Timer</code> (Zeile 19). Die Klasse <code>TimerTask</code> implementiert das Interface <code>Runnable</code>.</li>
<li><b>ScheduledExecutorService</b>: In Zeile 11 wird ein <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/ScheduledExecutorService.html"><code>ScheduledExecutorService</code></a> erzeugt, dem in diesem Fall genau ein Thread zur Ausführung der über ihn gesteuerten Runnables zur Verfügung steht. In Zeile 12 wird entsprechend des vorherigen Timer-Beispiels die wiederholte Ausführung eines Runnable gestartet. Die initiale Verzögerung beträgt wiederum 500 ms und das Intervall zwischen den Ausführungen 1 s. Der <code>ScheduledExecutorService</code> kann mittels der Methode <code>shutdown</code> beendet werden (Zeile 15).</li>
</ul>

<p>Das Interface <code>ScheduledExecutorService</code> erweitert das allgemeine funktionale Interface <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Executor.html"><code>Executor</code></a>, das lediglich die Methode <code>void execute(Runnable command)</code> spezifiziert, um ein Runnable nebenläufig auszuführen. Eine konkrete <code>Executor</code>- und <code>ScheduledExecutorService</code>-Implementierung ist die Klasse <code>ScheduledThreadPoolExecutor</code>, der im Konstruktor die Kapazität für einen <i>Thread-Pool</i> mitgegeben werden kann. Die Threads werden nun nicht mehr für je Runnable über einen Konstruktoraufruf <code>new Thread(...)</code> instantiiert, sondern aus einem begrenztem Thread-Pool wiederverwendet. Ein Thread-Pool bietet sich insbesondere für den typischen Anwendungsfall an, bei dem ein Server die Aufgaben (engl. <i>Tasks</i>) mehrerer Clients bedient. Das Thread-Pool-Muster vermeidet die ineffiziente Erzeugung eines neuen Threads je Aufgabe und schützt vor Ressourcenüberlastung, z.B. bei <i>Denial-of-Service</i>-Angriffen. Die Warteschlange, die zeitweise vor einem Thread-Pool entsteht, kann über unterschiedliche Strategien abgebaut werden, wobei <i>First-In-First-Out (FIFO)</i> häufig naheliegend und pragmatisch ist. Die folgende Abbildung verdeutlicht die Funktionsweise eines Thread-Pools.</p>

<img src="media/concurrency_pool.png" style="width:360px">
<label>Funktionsweise eines Thread-Pools</label>

<p>In dem folgenden Code-Beispiel wird die begrenzte Kapazität eines Thread-Pools demonstriert. Es sollen 7 Runnables gestartet werden (Zeilen 18-19). Der Thread-Pool lässt aber nur die Ausführung von 3 nebenläufigen Threads zu (Zeile 1), so dass die letzten 4 Runnables warten müssen, bis wieder ein Thread freigegeben wird. Da die Runnables jeweils zufällig lange arbeiten (Zeilen 8-11), ist nicht vorhersehbar, in welchem der 3 Threads die letzten 4 Runnables ausgeführt werden. Eine mögliche Ausgabe könnte wie folgt aussehen:</p>
<pre>Task 2 worked for 2522 ms in pool-1-thread-2. Total time: 2538 ms
Task 3 worked for 2617 ms in pool-1-thread-3. Total time: 2633 ms
Task 1 worked for 2916 ms in pool-1-thread-1. Total time: 2932 ms
Task 4 worked for 1734 ms in pool-1-thread-2. Total time: 4317 ms
Task 6 worked for 1596 ms in pool-1-thread-1. Total time: 4529 ms
Task 5 worked for 2552 ms in pool-1-thread-3. Total time: 5185 ms
Task 7 worked for 1498 ms in pool-1-thread-2. Total time: 5816 ms</pre>
 

<pre><code class="language-java line-numbers">ExecutorService executor = Executors.newFixedThreadPool(3); // thread pool restricted to 3 concurrent threads
AtomicInteger uniqueTaskId = new AtomicInteger(1);
LocalTime startTime = LocalTime.now();

// specify runnable
Runnable runnable = () -> {
	int id = uniqueTaskId.getAndIncrement();
	int sleepTime = ThreadLocalRandom.current().nextInt(1000, 3000); // random sleep time
	try {
		Thread.sleep(sleepTime); // simulate work
	} catch (InterruptedException e) { }

	long totalTime = startTime.until(LocalTime.now(), ChronoUnit.MILLIS);
	System.out.println("Task " + id + " worked for " + sleepTime + " ms in " + Thread.currentThread().getName() + ". Total time: " + totalTime + " ms");
};

// execute 7 runnables
for (int i = 7; i > 0; i--) { executor.execute(runnable); }
executor.shutdown();</code></pre>


<h4>Thread-lokale Variablen</h4>

<p>Eine Variable, die als Attribut eines Objekts (oder einer Klasse) auf dem Heap angelegt ist, kann von mehreren konkurrierenden Threads zeitgleich verändert werden, wodurch leider Inkonsistenzen in den Daten entstehen können. Der exklusive Zugriff auf derartige Variablen durch das Schlüsselwort <code>synchronized</code> wird im nächsten Kapitel zur <a href="#unit-sync" class="navigate">Synchronisation von Threads</a> im Detail vorgestellt. Im folgenden Code-Beispiel sichert das Schlüsselwort <code>synchronized</code> an der Methode <code>increment</code> (Zeile 7) zu, dass nur ein Thread zurzeit diese Methode ausführen kann. Variablen, die von einem Basisdatentyp sind und auf dem Stack gespeichert werden, liegen grundsätzlich je Thread vor. Sollen Objekte auf dem Heap je Thread instantiiert werden, bietet sich ein <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ThreadLocal.html"><code>ThreadLocal</code></a>-Objekt an. Das folgende Code-Beispiel demonstriert anschaulich, den Unterschied zwischen einer gemeinsamen Variable (Zeile 3) und einer Thread-lokalen Variable (Zeile 5). Der Aufruf der Methode <code>yield</code> in Zeile 20 löst einen Hinweis an den Scheduler aus, dass der aktive Thread bereit ist, seinen zugewiesenen Prozessor abzugeben.</p>

<pre><code class="language-java line-numbers">class MyThreadLocal {

    Integer common = 0; // shared resource of multiple threads

    ThreadLocal&lt;Integer> local = ThreadLocal.withInitial(() -> 0); // thread-local resource

    synchronized void increment() {
        common++;
        local.set(local.get() + 1);

        System.out.println(Thread.currentThread().getName() + ": common = " + common + ", local = " + local.get());
    }

    public static void main(String[] args) {
        MyThreadLocal resource = new MyThreadLocal();

        Runnable runnable = () -> {
            for (int i = 0; i &lt; 3; i++) {
                resource.increment();
                Thread.currentThread().yield(); // current thread is willing to yield its current use of a processor
            }
        };

        // start 3 concurrent threads
        for (int i = 0; i &lt; 3; i++) {
            new Thread(runnable).start();
        }
    }
}</code></pre>

<p>Die Ausgabe könnte z.B. wie folgt aussehen:</p>
<pre>Thread-0: common = 1, local = 1
Thread-2: common = 2, local = 1
Thread-1: common = 3, local = 1
Thread-0: common = 4, local = 2
Thread-1: common = 5, local = 2
Thread-0: common = 6, local = 3
Thread-2: common = 7, local = 2
Thread-1: common = 8, local = 3
Thread-2: common = 9, local = 3</pre>

<p>Die gezeigten Code-Beispiele zum Umgang mit Threads in Java finden sich im Verzeichnis <a href="/concurrency" class="repo-link">/concurrency</a> des Modul-Repository.</p>