9 virhettä, joita kannattaa välttää Java-ohjelmoinnissa
Mitä virheitä tulisi välttää Java-ohjelmoinnissa? Seuraavassa kappaleessa vastaamme tähän kysymykseen.
Lue ensimmäinen osa blogisarjastamme, joka on omistettu rinnakkaisuudelle Javassa. Seuraavassa artikkelissa tarkastelemme tarkemmin säikeiden ja prosessien välisiä eroja, säiepooleja, suorittajia ja paljon muuta!
Yleisesti ottaen perinteinen ohjelmointitapa on peräkkäinen. Kaikki ohjelmassa tapahtuu yksi vaihe kerrallaan.
Itse asiassa koko maailma toimii rinnakkain - se on kyky suorittaa useampi kuin yksi tehtävä samanaikaisesti.
Keskustelemme sellaisista edistyneistä aiheista kuin samanaikaisuus Java tai monisäikeistyksestä, meidän on sovittava joistakin yhteisistä määritelmistä, jotta voimme olla varmoja siitä, että olemme samalla sivulla.
Aloitetaan perusasioista. Ei-sekventiaalisessa maailmassa meillä on kahdenlaisia samanaikaisuuden edustajia: prosessit ja
langat. Prosessi on käynnissä olevan ohjelman instanssi. Tavallisesti se on eristetty muista prosesseista.
Käyttöjärjestelmä vastaa resurssien osoittamisesta kullekin prosessille. Lisäksi se toimii johtimena, joka
aikataulut ja valvoo niitä.
Lanka on eräänlainen prosessi, mutta alemmalla tasolla, joten sitä kutsutaan myös kevyeksi langaksi. Useita säikeitä voi toimia yhdessä
prosessi. Ohjelma toimii tässä tapauksessa säikeiden aikatauluttajana ja ohjaajana. Näin yksittäiset ohjelmat näyttävät tekevän
useita tehtäviä samanaikaisesti.
Säikeiden ja prosessien välinen perusero on eristystaso. Prosessilla on oma joukkonsa
resursseja, kun taas säie jakaa tietoja muiden säikeiden kanssa. Lähestymistapa saattaa vaikuttaa virhealttiilta, ja sitä se myös on. Esimerkiksi
Nyt ei keskitytä siihen, sillä se ei kuulu tähän artikkeliin.
Prosessit, säikeet - okei... Mutta mitä rinnakkaisuus oikeastaan on? Rinnakkaisuus tarkoittaa, että voit suorittaa useita tehtäviä samanaikaisesti.
aika. Se ei tarkoita, että tehtävien on suorituttava samanaikaisesti - sitä rinnakkaisuus on. Concurrenc in Javay ei myöskään
vaativat useita suorittimia tai jopa useita ytimiä. Se voidaan toteuttaa yhden ytimen ympäristössä hyödyntämällä seuraavia keinoja
kontekstin vaihtaminen.
Samanaikaisuuteen liittyvä termi on monisäikeistäminen. Se on ohjelmien ominaisuus, jonka avulla ne voivat suorittaa useita tehtäviä samanaikaisesti. Kaikki ohjelmat eivät käytä tätä lähestymistapaa, mutta niitä, jotka käyttävät sitä, voidaan kutsua monisäikeisiksi.
Olemme melkein valmiita lähtemään, enää yksi määritelmä. Asynkronisuus tarkoittaa, että ohjelma suorittaa muita kuin lukkiutumattomia toimintoja.
Se käynnistää tehtävän ja jatkaa sitten muita asioita odottaessaan vastausta. Kun se saa vastauksen, se voi reagoida siihen.
Oletusarvoisesti jokainen Java-sovellus toimii yhdessä prosessissa. Tässä prosessissa on yksi säie, joka liittyy ohjelmaan main()
menetelmä
hakemus. Kuten mainittu, on kuitenkin mahdollista hyödyntää useiden säikeiden mekanismeja yhdessä sovelluksessa.
ohjelma.
Lanka
on Java luokka, jossa taika tapahtuu. Tämä on edellä mainitun säikeen esitys. Osoitteeseen
luoda oman säikeen, voit laajentaa Lanka
luokka. Sitä ei kuitenkaan suositella. Kierteet
olisi käytettävä mekanismina, joka suorittaa tehtävän. Tehtävät ovat osia koodi jota haluamme ajaa rinnakkaistilassa. Voimme määritellä ne käyttämällä Käynnistettävä
rajapinta.
Mutta teoria riittää, laitetaanpa koodimme sinne, missä suumme on.
Oletetaan, että meillä on pari numeromäärää. Haluamme tietää kunkin matriisin numeroiden summan. Olkoon
Teeskennellään, että tällaisia matriiseja on paljon ja että jokainen niistä on suhteellisen suuri. Tällaisessa tilanteessa haluamme hyödyntää samanaikaisuutta ja laskea jokaisen matriisin yhteen erillisenä tehtävänä.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 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. 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();
Kuten yllä olevasta koodista näkyy Käynnistettävä
on toiminnallinen käyttöliittymä. Se sisältää yhden abstraktin menetelmän run()
ilman argumentteja. Osoite Käynnistettävä
rajapinnan tulisi olla toteutettuna kaikissa luokissa, joiden instanssit on tarkoitettu olemaan
säikeen suorittama.
Kun olet määritellyt tehtävän, voit luoda säikeen sen suorittamista varten. Tämä onnistuu uusi säie()
konstruktori, joka
ottaa Käynnistettävä
sen argumenttina.
Viimeinen vaihe on start()
äskettäin luotu säie. API:ssa on myös run()
menetelmät Käynnistettävä
jaLanka
. Se ei kuitenkaan ole tapa hyödyntää samanaikaisuutta Javassa. Suora kutsu kuhunkin näistä metodeista johtaa tulokseen
tehtävän suorittaminen samassa säikeessä, jossa main()
menetelmä toimii.
Kun tehtäviä on paljon, erillisen säikeen luominen jokaista tehtävää varten ei ole hyvä idea. Luominen Lanka
on
raskas operaatio, ja on paljon parempi käyttää olemassa olevia säikeitä uudelleen kuin luoda uusia.
Kun ohjelma luo monia lyhytikäisiä säikeitä, on parempi käyttää säiepoolia. Säiepooli sisältää useita
käyttövalmiita mutta tällä hetkellä ei aktiivisia säikeitä. Antamalla Käynnistettävä
pooliin aiheuttaa sen, että yksi säikeistä kutsuu komentoarun()
menetelmä tietyn Käynnistettävä
. Tehtävän suorittamisen jälkeen säie on edelleen olemassa ja joutokäyntitilassa.
Okei, ymmärrät kyllä - mieluummin säiepooli kuin manuaalinen luominen. Mutta miten voit hyödyntää säiepooleja? Osoitteessa Toimeenpanijat
luokassa on useita staattisia tehdasmenetelmiä säiepoolien rakentamista varten. Esimerkiksi newCachedThredPool()
luo
pooli, johon luodaan uusia säikeitä tarpeen mukaan ja tyhjänä olevia säikeitä pidetään 60 sekunnin ajan. Sitä vastoin,newFixedThreadPool()
sisältää kiinteän joukon säikeitä, joissa käyttämättömiä säikeitä säilytetään loputtomiin.
Katsotaanpa, miten se voisi toimia esimerkissämme. Nyt meidän ei tarvitse luoda säikeitä manuaalisesti. Sen sijaan meidän on luotavaExecutorService
joka tarjoaa säikeiden varannon. Sitten voimme antaa sille tehtäviä. Viimeinen vaihe on säikeen sulkeminen
poolia muistivuodon välttämiseksi. Muu osa edellisestä koodista pysyy samana.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(task1);
executor.submit(tehtävä2);
executor.submit(task3);
executor.shutdown();
Käynnistettävä
vaikuttaa näppärältä tavalta luoda samanaikaisia tehtäviä, mutta siinä on yksi merkittävä puute. Se ei voi palauttaa mitään
arvo. Lisäksi emme voi määrittää, onko tehtävä suoritettu vai ei. Emme myöskään tiedä, onko se suoritettu loppuun.
normaalisti tai poikkeuksellisesti. Ratkaisu näihin epäkohtiin on Soitettavissa
.
Soitettavissa
on samanlainen kuin Käynnistettävä
tavallaan myös asynkronisia tehtäviä. Tärkein ero on, että se pystyy
palauttaa arvon. Paluuarvo voi olla mitä tahansa (ei-primitiivistä) tyyppiä, kuten esimerkki Soitettavissa
rajapinta on parametrisoitu tyyppi.Soitettavissa
on toiminnallinen käyttöliittymä, jolla on call()
menetelmä, joka voi heittää Poikkeus
.
Katsotaanpa nyt, miten voimme hyödyntää Soitettavissa
joukko-ongelmassamme.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 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).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, näemme miten Soitettavissa
luodaan ja lähetetään sitten ExecutorService
. Mutta mitä hemmettiä on Tulevaisuus
?Tulevaisuus
toimii siltana säikeiden välillä. Kunkin matriisin summa tuotetaan erillisessä säikeessä, ja tarvitsemme keinon, jolla voimme
saada nämä tulokset takaisin main()
.
Tuloksen hakeminen osoitteesta Tulevaisuus
meidän on soitettava get()
menetelmä. Tässä voi tapahtua jompikumpi kahdesta asiasta. Ensinnäkin
laskennan tulos, jonka suorittaa Soitettavissa
on saatavilla. Sitten saamme sen välittömästi. Toiseksi, tulos ei ole
valmis vielä. Siinä tapauksessa get()
metodi estyy, kunnes tulos on saatavilla.
Kysymys Tulevaisuus
on, että se toimii "push-paradigman" mukaisesti. Kun käytetään Tulevaisuus
sinun on oltava kuin pomo, joka
kysyy jatkuvasti: "Onko tehtäväsi suoritettu? Onko se valmis?', kunnes se tuottaa tuloksen. Jatkuvan paineen alla toimiminen on
kallista. Paljon parempi lähestymistapa olisi tilata Tulevaisuus
mitä tehdä, kun se on valmis tehtäväänsä. Valitettavasti,Tulevaisuus
ei voi tehdä sitä, mutta ComputableFuture
voi.
ComputableFuture
toimii 'pull-paradigmassa'. Voimme kertoa sille, mitä tehdä tuloksella, kun se on suorittanut tehtävänsä. Se
on esimerkki asynkronisesta lähestymistavasta.
ComputableFuture
toimii täydellisesti Käynnistettävä
mutta ei Soitettavissa
. Sen sijaan on mahdollista antaa tehtävä osoitteeseenComputableFuture
muodossa Toimittaja
.
Katsotaanpa, miten edellä mainittu liittyy ongelmaamme.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 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).summa())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).summa())
.thenAccept(System.out::println);
Ensimmäisenä silmiinpistävänä asiana on, kuinka paljon lyhyempi tämä ratkaisu on. Sen lisäksi se näyttää myös siistiltä ja siistiltä.
Tehtävä CompletableFuture
voi antaa supplyAsync()
menetelmä, joka ottaa Toimittaja
tai runAsync()
että
ottaa Käynnistettävä
. Takaisinkutsu - koodinpätkä, joka on suoritettava tehtävän päättyessä - määritellään seuraavasti thenAccept()
menetelmä.
Java tarjoaa paljon erilaisia lähestymistapoja samanaikaisuuteen. Tässä artikkelissa käsittelimme aihetta hädin tuskin.
Käsittelimme kuitenkin perusasiat, jotka koskevat Lanka
, Käynnistettävä
, Soitettavissa
ja CallableFuture
mikä on hyvä seikka
aiheen jatkotutkimusta varten.