9 viga, mida vältida Java programmeerimisel
Milliseid vigu tuleks Java keeles programmeerimisel vältida? Järgnevas teoses vastame sellele küsimusele.

Loe meie blogisarja esimest osa, mis on pühendatud Java paralleelsusele. Järgnevas artiklis vaatleme lähemalt niidi ja protsessi erinevusi, niidipooli, täitjaid ja palju muud!
Üldiselt on tavapärane programmeerimismeetod järjestikune. Kõik programmis toimub üks samm korraga.
Kuid tegelikult on paralleel see, kuidas kogu maailm töötab - see on võime täita rohkem kui ühte ülesannet samaaegselt.
Arutleda selliste edasijõudnud teemade üle nagu paralleelsus Java või multithreading, peame leppima kokku mõnes ühises definitsioonis, et olla kindel, et oleme ühel ja samal leheküljel.
Alustame põhitõdedest. Mittesekventsionaalses maailmas on meil kahte liiki samaaegsuse esindajad: protsessid ja
niidid. Protsess on käimasoleva programmi instants. Tavaliselt on see teistest protsessidest isoleeritud.
Operatsioonisüsteem vastutab igale protsessile ressursside määramise eest. Lisaks sellele toimib see dirigendina, mis
ajakava ja kontrollib neid.
Niit on omamoodi protsess, kuid madalamal tasemel, seetõttu nimetatakse seda ka kergeks niidiks. Mitu niiti võib töötada ühes
protsess. Siinkohal tegutseb programm niitide ajaplaneerijana ja kontrollerina. Sel viisil näivad üksikud programmid teha
mitu ülesannet korraga.
Põhiline erinevus niitide ja protsesside vahel on isolatsioonitase. Protsessil on oma komplekt
ressursse, samas kui lõim jagab andmeid teiste lõimedega. See võib tunduda vigaohtlik lähenemine ja seda see ka on. Sest
nüüd ei keskendu me sellele, sest see ei kuulu käesoleva artikli reguleerimisalasse.
Protsessid, niidid - ok... Aga mis täpselt on samaaegsus? Samaaegsus tähendab, et saab täita mitu ülesannet korraga.
aeg. See ei tähenda, et need ülesanded peavad toimuma samaaegselt - see ongi paralleelsus. Concurrenc in Javay samuti ei ole
nõuavad, et teil oleks mitu protsessorit või isegi mitu südamikku. Seda on võimalik saavutada ühe tuumaga keskkonnas, kasutades ära
konteksti vahetamine.
Samaaegsusega seotud termin on mitmelaiendamine. See on programmide omadus, mis võimaldab neil täita korraga mitut ülesannet. Mitte iga programm ei kasuta seda lähenemist, kuid neid, mis seda teevad, võib nimetada multithreaded'iks.
Me oleme peaaegu valmis, vaid veel üks määratlus. Asünkroonsus tähendab, et programm sooritab mitteblokeerivaid operatsioone.
See algatab ülesande ja jätkab seejärel vastuse ootamise ajal muude asjadega. Kui ta saab vastuse, saab ta sellele reageerida.
Vaikimisi on iga Java rakendus töötab ühes protsessis. Selles protsessis on üks niit, mis on seotud main()
meetod
taotlus. Kuid, nagu mainitud, on võimalik kasutada mitme niidi mehhanisme ühes
programm.
Teema
on Java klass, kus toimub maagia. See on eelpool mainitud niidi objekti representatsioon. Et
luua oma lõim, saate laiendada Teema
klass. See ei ole siiski soovitatav lähenemisviis. Niidid
tuleks kasutada mehhanismi, mis täidab ülesannet. Ülesanded on tükid kood mida me tahame käivitada samaaegses režiimis. Me võime neid defineerida, kasutades Käivitatav
liides.
Kuid teooriast piisab, paneme oma koodi sinna, kus meie suu on.
Oletame, et meil on paar numbrite massiivi. Iga massiivi puhul tahame teada massiivi numbrite summat. Olgu
teeselda, et selliseid massiive on palju ja igaüks neist on suhteliselt suur. Sellistes tingimustes tahame kasutada samaaegsust ja summeerida iga massiivi eraldi ülesandena.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 3, 4, 3, 4, 2, 1, 3, 7};
Runnable task1 = () -> { {
int sum = Arrays.stream(a1).sum();
System.out.println("1. Summa on: " + sum);
};
Runnable task2 = () -> { {
int sum = Arrays.stream(a2).sum();
System.out.println("2. Summa on: " + sum);
};
Runnable task3 = () -> { {
int sum = Arrays.stream(a3).sum();
System.out.println("3. Summa on: " + sum);
};
new Thread(task1).start();
new Thread(task2).start();
new Thread(task3).start();
Nagu ülaltoodud koodist näha Käivitatav
on funktsionaalne liides. See sisaldab ühte abstraktset meetodit run()
ilma argumentideta. Veebileht Käivitatav
liidest peaks rakendama iga klass, mille instantsid on mõeldud kasutamiseks
mida teostab niit.
Kui olete määratlenud ülesande, saate selle käivitamiseks luua lõime. Seda saab teha järgmiselt new Thread()
konstruktor, mis
võtab Käivitatav
selle argumendiks.
Viimane samm on start()
äsja loodud teema. APIs on ka run()
meetodid Käivitatav
ja aastalTeema
. See ei ole aga viis, kuidas kasutada Java's samaaegsust. Otsekõne igale neist meetoditest annab tulemuseks
ülesande täitmine samas niidis, mis main()
meetod töötab.
Kui ülesandeid on palju, ei ole iga ülesande jaoks eraldi niidi loomine hea mõte. Luua Teema
on
raske operatsioon ja palju parem on olemasolevaid niite taaskasutada kui uusi luua.
Kui programm loob palju lühiajalisi niite, on parem kasutada niidipooli. Niidipool sisaldab mitmeid
käivitamisvalmis, kuid hetkel mitteaktiivsed niidid. Andmine Käivitatav
basseini põhjustab ühe niidi üleskutserun()
meetod antud Käivitatav
. Pärast ülesande täitmist on niit endiselt olemas ja on tühikäigul.
Okei, sa saad aru - eelista niidipooli käsitsi loomise asemel. Aga kuidas saab kasutada niidipooli? Veebileht Täitjad
klassil on mitmeid staatilisi tehasesiseseid meetodeid niidipoolide konstrueerimiseks. Näiteks newCachedThredPool()
loob
bassein, kuhu luuakse vajaduse korral uusi niite ja tühjaksjäänud niite hoitakse 60 sekundit. Seevastu,newFixedThreadPool()
sisaldab fikseeritud niitide kogumit, kus tühjad niidid hoitakse lõputult.
Vaatame, kuidas see meie näites toimida võiks. Nüüd ei pea me niite käsitsi looma. Selle asemel peame loomaExecutorService
mis pakub niitide kogumit. Seejärel saame sellele ülesandeid määrata. Viimane samm on niidi sulgemine
basseini, et vältida mälulekkeid. Ülejäänud eelmine kood jääb samaks.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.shutdown();
Käivitatav
näib olevat nutikas viis samaaegsete ülesannete loomiseks, kuid sellel on üks suur puudus. See ei saa tagastada ühtegi
väärtus. Veelgi enam, me ei saa kindlaks teha, kas ülesanne on lõpetatud või mitte. Samuti ei tea me, kas see on lõpetatud.
tavaliselt või erandkorras. Nende hädade lahendus on Väljakutsetav
.
Väljakutsetav
on sarnane Käivitatav
viisil ka asünkroonseid ülesandeid. Peamine erinevus seisneb selles, et see suudab
tagastada väärtus. Tagastusväärtus võib olla mis tahes (mitte-primitiivse) tüübiga, sest Väljakutsetav
liides on parameetriga tüüp.Väljakutsetav
on funktsionaalne liides, millel on call()
meetod, mis võib visata Erand
.
Nüüd vaatame, kuidas me saame võimendada Väljakutsetav
meie massiivi probleem.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).sum();
Callable task3 = () -> Arrays.stream(a3).sum();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. Summa on: " + future1.get());
System.out.println("2. Summa on: " + future2.get());
System.out.println("3. Summa on: " + future3.get());
executor.shutdown();
Okei, me näeme, kuidas Väljakutsetav
luuakse ja seejärel esitatakse ExecutorService
. Aga mis kurat on Tulevik
?Tulevik
toimib sildana lõimede vahel. Iga massiivi summa toodetakse eraldi niidiga ja meil on vaja viisi, kuidas teha
saada need tulemused tagasi main()
.
Tulemuse kättesaamiseks aadressilt Tulevik
me peame helistama get()
meetod. Siin võib juhtuda üks kahest asjast. Esiteks võib
arvutuste tulemus, mille on teinud Väljakutsetav
on saadaval. Siis me saame selle kohe. Teiseks, tulemus ei ole
veel valmis. Sel juhul get()
meetod blokeerib, kuni tulemus on saadaval.
Küsimus seoses Tulevik
on see, et see töötab "push-paradigmas". Kasutades Tulevik
sa pead olema nagu ülemus, kes
küsib pidevalt: "Kas teie ülesanne on täidetud? Kas see on valmis?", kuni see annab tulemuse. Pideva surve all tegutsemine on
kallis. Palju parem oleks tellida Tulevik
mida teha, kui ta on oma ülesandega valmis. Kahjuks,Tulevik
ei saa seda teha, kuid ComputableFuture
saab.
ComputableFuture
töötab "tõmbeparadigmas". Me saame öelda, mida teha tulemusega, kui see on oma ülesanded täitnud. See
on näide asünkroonsest lähenemisviisist.
ComputableFuture
töötab suurepäraselt koos Käivitatav
kuid mitte Väljakutsetav
. Selle asemel on võimalik anda ülesanne, etComputableFuture
kujul Tarnija
.
Vaatame, kuidas ülaltoodu on seotud meie probleemiga.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 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);
Esimene asi, mis silma torkab, on see, kui palju lühem on see lahendus. Peale selle näeb see ka puhas ja korralik välja.
Ülesanne ValmisTulevik
võib anda supplyAsync()
meetod, mis võtab Tarnija
või runAsync()
et
võtab Käivitatav
. Tagasikutsumine - kood, mis tuleb käivitada ülesande lõpetamisel - on defineeritud järgmiselt. thenAccept()
meetod.
Java pakub palju erinevaid lähenemisviise samaaegsusele. Selles artiklis me vaevu puudutasime seda teemat.
Sellegipoolest käsitlesime põhitõdesid Teema
, Käivitatav
, Väljakutsetav
ja CallableFuture
mis on hea mõte
teema edasiseks uurimiseks.