Skip to content

Opdracht 15 - Threads

Doel

In deze opdracht ga je aan de slag met threads in Java om te begrijpen hoe threads werken en reageren. In het eerste gedeelte programmeer je zelf een thread en gebruik je deze. In het tweede gedeelte krijg je een programma van ons waarmee je Amdahl's Law kan demonstreren.

Na deze opdracht kan je:

  • Een thread-klasse aanmaken als subklasse van Thread.
  • Begrijpen wanneer threads wel of niet op elkaar wachten.
  • Inzien dat threads de uitvoersnelheid slechts tot een bepaald punt kunnen versnellen.

Opdracht

💡 Merk op: je hoeft deze opgave niet in een virtuele machine uit te voeren, je hebt nl. reeds Java geïnstalleerd op je computer. Je kan de bestanden dus gewoon op je eigen computer bewerken en compileren.

Programmeren van threads

Je vindt informatie over het programmeren van threads op volgende links, lees deze eerst door:

Je krijgt van ons enkele Java-bestanden, te vinden in opgave.zip. Je kan deze importeren in een IDE naar keuze (Eclipse, JetBrains, NetBeans...) of aanpassen met een text editor en compileren op de command line. Compileren kan met het commando javac, uitvoeren met java, zoals in onderstaand voorbeeld:

Bash
javac FindPrimesThread.java
java FindPrimesThread

De gegeven bestanden zijn onvolledig, je zal zelf enkele lijnen code moeten invullen. De instructies staan in de bestanden zelf, in de commentaar die start met TODO:. Het programma berekent van een reeks getallen hoeveel ervan een priemgetal zijn, bv. in de reeks van getallen 2 t.e.m. 100000 zijn er 9592 getallen een priemgetal.

De startcode bevat drie bestanden:

  • PrimeCounter.java: dit bestand bevat de code om het aantal priemgetallen te detecteren in het interval [min, max[. Je hoeft aan dit bestand niets te veranderen, maar je kan het wel gebruiken in FindPrimesThread.java.
  • FindPrimesThread.java: dit bestand moet je helemaal zelf aanvullen. Deze klasse is een subklasse van Thread en heeft enkele private attributen. Deze klasse heeft een constructor die een id-nummer, het mininumnummer van de reeks en het maximumnummer van de reeks meekrijgt. Wanneer de thread wordt uitgevoerd, berekent die het aantal priemgetallen in de reeks en toont het resultaat en zijn id op het scherm. Je kan de klasse PrimeCounter gebruiken om het aantal priemgetallen te berekenen.
  • MultiThreading.java: dit bestand bevat de main-methode. In deze methode moet je enkele kleine aanpassingen maken, zoals het aanmaken en starten van de FindPrimesThread threads.

De bedoeling is dat je een gelijkaardige uitvoer bekomt als je het programma driemaal uitvoert:

Bash Session
$ javac MultiThreading.java
$ java MultiThreading
Creating threads ...
Threads created.
Starting threads ...
Threads 0 has started.
Threads 1 has started.
Threads 2 has started.
Threads 3 has started.
Threads 4 has started.
Threads 5 has started.
Threads 6 has started.
Threads 7 has started.
Threads 8 has started.
Threads 9 has started.
Threads have started.
This could take a while: please wait ...
Thread 2 found 9592 primes in the range [2, 100000[
Thread 9 found 9592 primes in the range [2, 100000[
Thread 3 found 9592 primes in the range [2, 100000[
Thread 5 found 9592 primes in the range [2, 100000[
Thread 6 found 9592 primes in the range [2, 100000[
Thread 0 found 9592 primes in the range [2, 100000[
Thread 8 found 9592 primes in the range [2, 100000[
Thread 4 found 9592 primes in the range [2, 100000[
Thread 7 found 9592 primes in the range [2, 100000[
Thread 1 found 9592 primes in the range [2, 100000[
All threads have finished.

$ java MultiThreading
Creating threads ...
Threads created.
Starting threads ...
Threads 0 has started.
Threads 1 has started.
Threads 2 has started.
Threads 3 has started.
Threads 4 has started.
Threads 5 has started.
Threads 6 has started.
Threads 7 has started.
Threads 8 has started.
Threads 9 has started.
Threads have started.
This could take a while: please wait ...
Thread 4 found 9592 primes in the range [2, 100000[
Thread 6 found 9592 primes in the range [2, 100000[
Thread 2 found 9592 primes in the range [2, 100000[
Thread 3 found 9592 primes in the range [2, 100000[
Thread 9 found 9592 primes in the range [2, 100000[
Thread 1 found 9592 primes in the range [2, 100000[
Thread 8 found 9592 primes in the range [2, 100000[
Thread 5 found 9592 primes in the range [2, 100000[
Thread 0 found 9592 primes in the range [2, 100000[
Thread 7 found 9592 primes in the range [2, 100000[
All threads have finished.

$ java MultiThreading
Creating threads ...
Threads created.
Starting threads ...
Threads 0 has started.
Threads 1 has started.
Threads 2 has started.
Threads 3 has started.
Threads 4 has started.
Threads 5 has started.
Threads 6 has started.
Threads 7 has started.
Threads 8 has started.
Threads 9 has started.
Threads have started.
This could take a while: please wait ...
Thread 5 found 9592 primes in the range [2, 100000[
Thread 3 found 9592 primes in the range [2, 100000[
Thread 2 found 9592 primes in the range [2, 100000[
Thread 9 found 9592 primes in the range [2, 100000[
Thread 8 found 9592 primes in the range [2, 100000[
Thread 6 found 9592 primes in the range [2, 100000[
Thread 1 found 9592 primes in the range [2, 100000[
Thread 7 found 9592 primes in the range [2, 100000[
Thread 0 found 9592 primes in the range [2, 100000[
Thread 4 found 9592 primes in the range [2, 100000[
All threads have finished.
Merk je dat de threads in volgorde opstarten, maar eindigen ze ook telkens in dezelfde volgorde?

Dit komt omdat de threads onafhankelijk van elkaar uitgevoerd worden. De volgorde waarin de threads uitgevoerd worden is afhankelijk van de keuzes die de scheduler maakt. Herinner je: de scheduler bepaalt welke thread op welk moment uitgevoerd wordt. De threads zijn dus niet synchroon, en de volgorde is onvoorspelbaar.

In MultiThreading.java wordt de string Threads have started. direct geprint, maar de string All threads have finished. pas nadat alle threads zijn voltooid.

Hoe komt dit, aangezien beide strings na de code om de threads te starten staan?

De methode start() van een thread-object zorgt ervoor dat de thread wordt gestart, maar de code in de thread zelf wordt asynchroon uitgevoerd. De methode join() zorgt ervoor dat de thread waarop deze methode wordt aangeroepen wacht tot de thread waarop de methode is aangeroepen klaar is. In dit geval wordt de methode join() aangeroepen op elke thread in de for-loop in MultiThreading.java. Dit zorgt ervoor dat de main-thread wacht tot alle threads klaar zijn met hun berekeningen.

Het is inderdaad zinloos om speciaal threads aan te maken om meerdere keren dezelfde berekening uit te voeren. Dit programma dient enkel als oefening voor het aanmaken en begrijpen van threads. Er wordt bijvoorbeeld geen gebruik gemaakt van gedeelde variabelen tussen threads, omdat dit het programma aanzienlijk complexer zou maken.

Waarom is dat zo?

Als threads gedeelde variabelen gebruiken, kan het gebeuren dat de ene thread de waarde van een variabele aanpast terwijl een andere thread deze waarde leest. Dit kan leiden tot onvoorspelbaar gedrag. Om dit te voorkomen, kan je gebruik maken van synchronisatie, maar dit maakt het programma complexer en kan leiden tot deadlocks.

De oplossing van deze opgave vind je hier.

Berekenen Amdahl's Law

❗️ Let op: dit deel van de opgave over Amdahl's Law gaat ervan uit dat je JDK 21 geïnstalleerd hebt (zoals bij OOSD). Met een oudere versie van Java zal het programma niet werken.

Je krijgt van ons een jar-bestand, te vinden in MultiThreading.jar dat een complexe taak uitvoert met behulp van multithreading. De taak wordt verdeeld over een aantal threads en elke thread voert vervolgens zijn deel uit. Bij de start van het programma kan je het aantal threads opgeven. Je kan het programma uitvoeren met het volgende commando:

Bash
java -jar MultiThreading.jar

Maak bv. in Excel, Calc... een tabel aan met het aantal threads en de uitvoeringstijd van het programma. Om er zeker van te zijn dat je geen ongeldige uitschieters in jouw resultaten hebt, voer elke meting voor een bepaald aantal threads meerdere keren uit en neem het gemiddelde.

Dit is bijvoorbeeld het resultaat bij gebruik van een Intel i7-6700K processor:

Aantal threads Aantal seconden
1 10,334
2 5,586
3 3,952
4 3,162
5 2,855
6 2,613
7 2,359
8 2,302
10 2,34
12 2,345
14 2,381
16 2,245

Maak met behulp van een programma naar keuze (Excel, Calc...) een grafiek van deze tabel met het aantal threads als x-as en het aantal seconden als y-as. Wat zie je? Voor het voorbeeld van de Intel i7-6700K processor geeft dit het volgende:

Voorbeeld uitvoeringstijd Intel i7-6700K processor

Wat is het laagste aantal seconden dat je kan behalen? Wat betekent dit? Wat is de link met Amdahl's Law?

Het laagste aantal seconden dat je kan behalen hangt van je eigen systeem af. Dit is de tijd die nodig is voor de algemene code voor het beheer van de threads. Je hebt nl. steeds een deel van de code dat je niet kan versnellen. Dit percentage van de code geef je mee aan de variabele \(S\) in Amdahl's Law.

Wat gebeurt er als je echt heel veel threads laat aanmaken: kan het programma dit aan?

Het programma kan dit niet aan. Het aantal threads dat je kan aanmaken is beperkt door de hoeveelheid geheugen die je systeem heeft en het Java-programma toegewezen kreeg. Elke thread heeft een eigen stack, en als je te veel threads aanmaakt, kan je systeem vastlopen.


Bronnen