9 fouten die je moet vermijden bij het programmeren in Java
Welke fouten moet je vermijden bij het programmeren in Java? In het volgende stuk beantwoorden we deze vraag.
Lees het eerste deel van onze blogserie over concurrency in Java. In het volgende artikel gaan we dieper in op de verschillen tussen threads en processen, thread pools, executors en nog veel meer!
In het algemeen is de conventionele programmeeraanpak sequentieel. Alles in een programma gebeurt stap voor stap.
Maar in feite draait de hele wereld parallel - het is de mogelijkheid om meer dan één taak tegelijkertijd uit te voeren.
Om geavanceerde onderwerpen te bespreken zoals gelijktijdigheid in Java of multithreading, moeten we het eens worden over enkele gemeenschappelijke definities om er zeker van te zijn dat we op dezelfde pagina zitten.
Laten we beginnen met de basis. In de niet-sequentiële wereld hebben we twee soorten concurrency representanten: processen en
threads. Een proces is een instantie van het programma dat draait. Normaal gesproken is het geïsoleerd van andere processen.
Het besturingssysteem is verantwoordelijk voor het toewijzen van bronnen aan elk proces. Bovendien fungeert het als een geleider die
plant en controleert ze.
Thread is een soort proces, maar op een lager niveau, daarom wordt het ook wel light thread genoemd. Meerdere threads kunnen in één
proces. Hier fungeert het programma als een scheduler en een controller voor threads. Op deze manier lijkt het alsof individuele programma's
meerdere taken tegelijkertijd.
Het fundamentele verschil tussen threads en processen is het isolatieniveau. Het proces heeft zijn eigen set van
bronnen, terwijl de thread gegevens deelt met andere threads. Het lijkt misschien een foutgevoelige aanpak en dat is het ook. Voor
Laten we ons daar nu niet op richten, want dat valt buiten het bestek van dit artikel.
Processen, threads - ok... Maar wat is concurrency precies? Concurrency betekent dat je meerdere taken tegelijk kunt uitvoeren.
tijd. Het betekent niet dat die taken gelijktijdig moeten worden uitgevoerd - dat is wat parallellisme is. Concurrenc in Javay ook niet
meerdere CPU's of zelfs meerdere cores nodig. Het kan worden bereikt in een single-core omgeving door gebruik te maken van
contextschakelen.
Een term die gerelateerd is aan concurrency is multithreading. Dit is een eigenschap van programma's waarmee ze meerdere taken tegelijk kunnen uitvoeren. Niet elk programma gebruikt deze aanpak, maar de programma's die dat wel doen, kunnen multithreaded worden genoemd.
We zijn bijna klaar, nog één definitie. Asynchronie betekent dat een programma niet-blokkerende bewerkingen uitvoert.
Het initieert een taak en gaat dan verder met andere dingen terwijl het wacht op het antwoord. Wanneer het de reactie krijgt, kan het erop reageren.
Standaard wordt elke Java-toepassing draait in één proces. In dat proces is er één thread gerelateerd aan de hoofd()
methode van
een applicatie. Zoals gezegd is het echter mogelijk om gebruik te maken van de mechanismen van meerdere threads binnen één
programma.
Draad
is een Java klasse waarin de magie gebeurt. Dit is de objectrepresentatie van de eerder genoemde thread. Naar
je eigen thread maken, kun je de Draad
klasse. Het is echter geen aanbevolen aanpak. Draden
moet worden gebruikt als een mechanisme om de taak uit te voeren. Taken zijn stukjes code die we in een gelijktijdige modus willen uitvoeren. We kunnen ze definiëren met de Runnable
interface.
Maar genoeg theorie, laten we de daad bij het woord voegen.
Stel dat we een aantal matrices met getallen hebben. Voor elke matrix willen we de som van de getallen in een matrix weten. Laten we
doen alsof er veel van zulke arrays zijn en elk ervan relatief groot is. In dergelijke omstandigheden willen we gebruik maken van concurrency en elke array als een aparte taak optellen.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Runnable task1 = () -> {
int sum = Arrays.stream(a1).sum();
System.out.println("1. De som is: " + som);
};
Runnable task2 = () -> {
int sum = Arrays.stream(a2).sum();
System.out.println("2. De som is: " + som);
};
Runnable task3 = () -> {
int sum = Arrays.stream(a3).sum();
System.out.println("3. De som is: " + som);
};
new Thread(task1).start();
nieuwe Thread(task2).start();
nieuwe Thread(task3).start();
Zoals je kunt zien in de bovenstaande code Runnable
is een functionele interface. Het bevat één abstracte methode uitvoeren()
zonder argumenten. De Runnable
interface moet geïmplementeerd worden door elke klasse waarvan de instanties bedoeld zijn om
uitgevoerd door een thread.
Zodra je een taak hebt gedefinieerd, kun je een thread maken om de taak uit te voeren. Dit kan via nieuwe draad()
constructor die
neemt Runnable
als argument.
De laatste stap is start()
een nieuw aangemaakte thread. In de API zijn er ook uitvoeren()
methoden in Runnable
en inDraad
. Dat is echter geen manier om gebruik te maken van concurrency in Java. Een directe aanroep naar elk van deze methoden resulteert in
de taak uitvoeren in dezelfde thread als de hoofd()
methode wordt uitgevoerd.
Als er veel taken zijn, is het geen goed idee om voor elke taak een aparte thread aan te maken. Een Draad
is een
en het is veel beter om bestaande threads te hergebruiken dan om nieuwe te maken.
Als een programma veel kortstondige threads aanmaakt, is het beter om een threadpool te gebruiken. De threadpool bevat een aantal
threads die klaar zijn om te worden uitgevoerd, maar momenteel niet actief zijn. Een Runnable
naar de pool zorgt ervoor dat een van de threads deuitvoeren()
methode van gegeven Runnable
. Na het voltooien van een taak bestaat de thread nog steeds en bevindt zich in een inactieve toestand.
Ok, je snapt het - liever een threadpool dan handmatig aanmaken. Maar hoe kun je threadpools gebruiken? De Executeurs
klasse heeft een aantal statische fabrieksmethoden voor het bouwen van threadpools. Bijvoorbeeld nieuweCachedThredPool()
creëert
een pool waarin nieuwe threads worden aangemaakt als dat nodig is en niet-actieve threads 60 seconden worden vastgehouden. In tegenstelling,newFixedThreadPool()
bevat een vaste set threads, waarin idle threads voor onbepaalde tijd worden bewaard.
Laten we eens kijken hoe het in ons voorbeeld zou kunnen werken. Nu hoeven we threads niet handmatig aan te maken. In plaats daarvan moeten weUitvoerderService
die zorgt voor een pool van threads. Vervolgens kunnen we er taken aan toewijzen. De laatste stap is het sluiten van de thread
pool om geheugenlekken te voorkomen. De rest van de vorige code blijft hetzelfde.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.shutdown();
Runnable
lijkt een handige manier om gelijktijdige taken te maken, maar het heeft één grote tekortkoming. Het kan geen
waarde. Bovendien kunnen we niet bepalen of een taak voltooid is of niet. We weten ook niet of de taak is voltooid
normaal of uitzonderlijk. De oplossing voor deze kwalen is Opvraagbaar
.
Opvraagbaar
is vergelijkbaar met Runnable
op een bepaalde manier ook asynchrone taken. Het belangrijkste verschil is dat het in staat is om
een waarde teruggeven. De retourwaarde kan van elk (niet-primitief) type zijn als de Opvraagbaar
interface is een geparametriseerd type.Opvraagbaar
is een functionele interface die oproep()
methode die een Uitzondering
.
Laten we nu eens kijken hoe we gebruik kunnen maken van Opvraagbaar
in ons matrixprobleem.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).som();
Callable task3 = () -> Arrays.stream(a3).som();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. De som is: " + future1.get());
System.out.println("2. De som is: " + future2.get());
System.out.println("3. De som is: " + future3.get());
executor.shutdown();
Oké, we kunnen zien hoe Opvraagbaar
wordt gemaakt en vervolgens ingediend bij UitvoerderService
. Maar wat is Toekomst
?Toekomst
fungeert als een brug tussen threads. De som van elke array wordt geproduceerd in een aparte thread en we hebben een manier nodig om
die resultaten terugsturen naar hoofd()
.
Om het resultaat op te halen uit Toekomst
moeten we bellen krijgen()
methode. Hier kunnen twee dingen gebeuren. Ten eerste wordt de
resultaat van de berekening uitgevoerd door Opvraagbaar
beschikbaar is. Dan krijgen we het onmiddellijk. Ten tweede is het resultaat niet
nog niet klaar. In dat geval krijgen()
methode zal blokkeren totdat het resultaat beschikbaar is.
Het probleem met Toekomst
is dat het werkt in het 'push-paradigma'. Bij gebruik van Toekomst
je moet zijn als een baas die
vraagt voortdurend: "Is je taak klaar? Is het klaar?" totdat het resultaat oplevert. Handelen onder constante druk is
duur. Een veel betere aanpak zou zijn om Toekomst
wat te doen als het klaar is met zijn taak. Helaas,Toekomst
kan dat niet doen, maar ComputableFuture
kan.
ComputableFuture
werkt in het 'pull-paradigma'. We kunnen het vertellen wat te doen met het resultaat wanneer het zijn taken heeft voltooid. Het
is een voorbeeld van een asynchrone aanpak.
ComputableFuture
werkt perfect met Runnable
maar niet met Opvraagbaar
. In plaats daarvan is het mogelijk om een taak te leveren aanComputableFuture
in de vorm van Leverancier
.
Laten we eens kijken hoe het bovenstaande verband houdt met ons probleem.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
CompletableFuture.supplyAsync() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println);
Het eerste wat opvalt is hoeveel korter deze oplossing is. Daarnaast ziet het er ook nog eens netjes en opgeruimd uit.
Taak naar InvulbaarToekomst
kan worden geleverd door supplyAsync()
methode die Leverancier
of door runAsync()
dat
neemt Runnable
. Een callback - een stukje code dat moet worden uitgevoerd na het voltooien van een taak - wordt gedefinieerd door thenAccept()
methode.
Java biedt veel verschillende benaderingen van gelijktijdigheid. In dit artikel hebben we het onderwerp nauwelijks aangestipt.
Niettemin hebben we de basisbeginselen van Draad
, Runnable
, Opvraagbaar
en CallableFuture
wat een goed punt is
voor verder onderzoek.