Ohjelmointi

Java 101: Java-samanaikaisuus ilman kipua, osa 1

Samanaikaisten sovellusten monimutkaisuuden myötä monet kehittäjät huomaavat, että Javan matalan tason ketjutusominaisuudet eivät riitä heidän ohjelmointitarpeisiinsa. Siinä tapauksessa saattaa olla aika löytää Java Concurrency Utilities. Aloita java.util.concurrent, Jeff Friesenin yksityiskohtaisen johdannon Executor-kehykseen, synkronointityyppeihin ja Java Concurrent Collections -pakettiin.

Java 101: Seuraava sukupolvi

Tämän uuden JavaWorld-sarjan ensimmäinen artikkeli esittelee Java Date and Time -sovellusliittymä.

Java-alusta tarjoaa matalan tason ketjutusominaisuudet, joiden avulla kehittäjät voivat kirjoittaa samanaikaisia ​​sovelluksia, joissa eri ketjut suoritetaan samanaikaisesti. Tavallisella Java-ketjutuksella on kuitenkin joitain haittapuolia:

  • Javan matalan tason samanaikaisuuden primitiivit (synkronoitu, haihtuva, odota(), ilmoittaa()ja Ilmoita kaikille ()) ei ole helppoa käyttää oikein. Myös primitiivien väärästä käytöstä johtuvat langankierron vaarat, kuten umpikuja, langan nälänhätä ja kilpailuolosuhteet, on vaikea havaita ja korjata.
  • Luottaa johonkin synkronoitu ketjujen välisen pääsyn koordinointi johtaa suorituskykyongelmiin, jotka vaikuttavat sovellusten skaalautuvuuteen, mikä on vaatimus monille nykyaikaisille sovelluksille.
  • Java-ketjun perusominaisuudet ovat liian matala taso. Kehittäjät tarvitsevat usein korkeamman tason rakenteita, kuten semaforeja ja ketjuja, joita Java ei tarjoa matalan tason ketjutustoiminnoissa. Tämän seurauksena kehittäjät rakentavat omat rakenteensa, mikä on sekä aikaa vievää että virhealtista.

JSR 166: Concurrency Utilities -kehys on suunniteltu vastaamaan korkean tason ketjutuksen tarpeeseen. Vuoden 2002 alussa aloitettu kehys virallistettiin ja otettiin käyttöön kaksi vuotta myöhemmin Java 5: ssä. Java 6: n, Java 7: n ja tulevan Java 8: n parannuksia on seurattu.

Tämä kaksiosainen Java 101: Seuraava sukupolvi -sarja esittelee ohjelmistokehittäjät, jotka ovat perehtyneet Java-ketjuun, Java Concurrency Utilities -paketeihin ja kehyksiin. Osassa 1 esitän yleiskuvan Java Concurrency Utilities -kehyksestä ja esitän sen Executor-kehyksen, synkronointiapuohjelmat ja Java Concurrent Collections -paketin.

Java-ketjujen ymmärtäminen

Ennen kuin sukellat tähän sarjaan, varmista, että olet perehtynyt langoituksen perusteisiin. Aloita Java 101 Johdanto Java: n matalan tason ketjutusominaisuuksiin:

  • Osa 1: Esittely kierteet ja ajettavat
  • Osa 2: Langan synkronointi
  • Osa 3: Langan ajoitus, odotus / ilmoitus ja langan keskeytys
  • Osa 4: Lankaryhmät, volatiliteetti, säie-paikalliset muuttujat, ajastimet ja langan kuolema

Java Concurrency Utilities -sovelluksen sisällä

Java Concurrency Utilities -kehys on kirjasto tyypit jotka on suunniteltu käytettäviksi rakennuspalikoina samanaikaisten luokkien tai sovellusten luomiseen. Nämä tyypit ovat kierteille turvallisia, testattu perusteellisesti ja tarjoavat korkean suorituskyvyn.

Java Concurrency Utilities -tyypit on järjestetty pieniin kehyksiin; nimittäin Executor-kehys, synkronoija, samanaikaiset kokoelmat, lukot, atomimuuttujat ja Fork / Join. Ne on edelleen järjestetty pääpakettiin ja pariksi alipaketteja:

  • java.util.concurrent sisältää korkean tason apuohjelmatyyppejä, joita käytetään yleisesti samanaikaisessa ohjelmoinnissa. Esimerkkejä ovat semaforit, esteet, lanka-altaat ja samanaikaiset hashmapit.
    • java.util.concurrent.atomic alipaketti sisältää matalatasoisia apuohjelmaluokkia, jotka tukevat lukitsemattomia langattomia ohjelmointeja yksittäisille muuttujille.
    • java.util.concurrent.locks alipaketti sisältää matalan tason apuohjelmatyypit lukitsemiseksi ja olosuhteiden odottamiseksi, jotka poikkeavat Javan matalan tason synkronoinnin ja näyttöjen käytöstä.

Java Concurrency Utilities -kehys paljastaa myös matalan tason vertaa ja vaihda (CAS) laitteisto-ohjeet, joiden muunnelmia nykyaikaiset prosessorit tukevat yleisesti. CAS on paljon kevyempi kuin Java: n monitoripohjainen synkronointimekanismi, ja sitä käytetään joidenkin erittäin skaalautuvien samanaikaisten luokkien toteuttamiseen. CAS-pohjainen java.util.concurrent.locks.ReentrantLock Esimerkiksi luokka on suorituskykyisempi kuin vastaava näyttöpohjainen synkronoitu primitiivinen. ReentrantLock tarjoaa paremman lukituksen hallinnan. (Osassa 2 selitän lisää siitä, miten CAS toimii java.util.concurrent.)

System.nanoTime ()

Java Concurrency Utilities -kehys sisältää pitkä nanoTime (), joka on java.lang.Järjestelmä luokassa. Tämä menetelmä mahdollistaa pääsyn nanosekunnin rakeisuuden aikalähteeseen suhteellisten aikamittausten tekemiseksi.

Seuraavissa osioissa esittelen Java Concurrency Utilities -ohjelman kolme hyödyllistä ominaisuutta, selittäen ensin, miksi ne ovat niin tärkeitä nykyaikaiselle samanaikaisuudelle, ja osoittamalla sitten, miten ne työskentelevät samanaikaisten Java-sovellusten nopeuden, luotettavuuden, tehokkuuden ja skaalautuvuuden lisäämiseksi.

Executor-kehys

Langoituksessa a tehtävä on työn yksikkö. Yksi Java-tason matalan tason ketjutuksen ongelmista on, että tehtävien lähettäminen liittyy tiiviisti tehtävän suorittamispolitiikkaan, kuten Listing 1 osoittaa.

Listing 1. Server.java (versio 1)

tuo java.io.IOException; tuo java.net.ServerSocket; tuo java.net.Socket; luokka Palvelin {public static void main (String [] argumentit) heittää IOException {ServerSocket socket = new ServerSocket (9000); while (true) {lopullinen Socket s = socket.accept (); Runnable r = uusi Runnable () {@Override public void run () {doWork (s); }}; uusi lanka (r) .start (); }} staattinen void doWork (Socket s) {}}

Yllä oleva koodi kuvaa yksinkertaista palvelinsovellusta (kanssa doWork (pistorasia) jätetty tyhjäksi lyhyeksi). Palvelinketju kutsuu toistuvasti socket.accept () odottaa saapuvaa pyyntöä ja käynnistää sitten ketjun palvelemaan tätä pyyntöä, kun se saapuu.

Koska tämä sovellus luo uuden ketjun jokaiselle pyynnölle, se ei skaalaudu hyvin, kun edessä on valtava määrä pyyntöjä. Esimerkiksi jokainen luotu ketju vaatii muistia, ja liian monta ketjua saattaa tyhjentää käytettävissä olevan muistin, mikä pakottaa sovelluksen lopettamaan.

Voit ratkaista tämän ongelman muuttamalla tehtävän suorittamisen käytäntöä. Uuden viestiketjun luomisen sijasta voit käyttää säikejoukkoa, jossa kiinteä määrä säikeitä palvelisi saapuvia tehtäviä. Sinun on kuitenkin kirjoitettava sovellus muutoksen tekemiseksi.

java.util.concurrent sisältää Executor-kehyksen, pienen kehyksen tyypeistä, jotka erottavat tehtävän lähetyksen tehtävän suorittamisen käytännöistä. Executor-kehyksen avulla on helppo virittää ohjelman tehtävänsuorituskäytäntö ilman, että sinun tarvitsee kirjoittaa koodia merkittävästi.

Executor-kehyksen sisällä

Executor-kehys perustuu Suorittaja käyttöliittymä, joka kuvaa toimeenpanija kuten mikä tahansa esine, joka pystyy suorittamaan java.lang.Runnable tehtäviä. Tämä liitäntä ilmoittaa seuraavan yksinäisen menetelmän a Ajettava tehtävä:

void execute (Suoritettava komento)

Lähetät a Ajettava tehtävän välittämällä sen suorittaa (ajettava). Jos toteuttaja ei pysty suorittamaan tehtävää jostain syystä (esimerkiksi jos suorittaja on suljettu), tämä menetelmä heittää HylättyExecutionException.

Keskeinen käsite on se tehtävän lähettäminen on irrotettu tehtävän suorituskäytännöstä, jota kuvaa Suorittaja toteutus. ajettava Tehtävä pystyy siis suorittamaan uuden säikeen, yhdistetyn säiön, kutsuvan säikeen ja niin edelleen.

Ota huomioon, että Suorittaja on hyvin rajallinen. Et voi esimerkiksi sulkea suoritinta tai määrittää, onko asynkroninen tehtävä päättynyt. Et voi myöskään peruuttaa käynnissä olevaa tehtävää. Näistä ja muista syistä Executor-kehys tarjoaa ExecutorService-käyttöliittymän, joka laajenee Suorittaja.

Viisi ExecutorServiceMenetelmät ovat erityisen huomionarvoisia:

  • looginen odota Lopetus (pitkä aikakatkaisu, TimeUnit-yksikkö) estää kutsuvan säikeen, kunnes kaikki tehtävät on suoritettu loppuun sammutuspyynnön jälkeen, aikakatkaisu tapahtuu tai nykyinen ketju keskeytyy sen mukaan, kumpi tapahtuu ensin. Odotuksen enimmäisaika on määritetty Aikalisä, ja tämä arvo ilmaistaan yksikkö yksikön määrittelemät yksiköt TimeUnit enum; esimerkiksi, TimeUnit SEKUNDIT. Tämä menetelmä heittää java.lang.InterruptedException kun nykyinen ketju keskeytetään. Se palaa totta kun toteuttaja irtisanotaan ja väärä kun aikakatkaisu umpeutuu ennen lopettamista.
  • looginen isShutdown () palaa totta kun teloittaja on suljettu.
  • tyhjä sammutus () käynnistää järjestäytyneen sammutuksen, jossa aiemmin lähetetyt tehtävät suoritetaan, mutta uusia tehtäviä ei hyväksytä.
  • Tuleva lähetys (soitettava tehtävä) lähettää arvon palauttavan tehtävän suoritettavaksi ja palauttaa a Tulevaisuus edustaa tehtävän odottavia tuloksia.
  • Tuleva lähetys (suoritettava tehtävä) esittää Ajettava tehtävä suoritettavaksi ja palauttaa a Tulevaisuus edustavat kyseistä tehtävää.

Tulevaisuus rajapinta edustaa asynkronisen laskennan tulosta. Tulos tunnetaan nimellä tulevaisuudessa koska se on tyypillisesti saatavana vasta tulevaisuudessa. Voit käyttää tapoja peruuttaa tehtävä, palauttaa tehtävän tulos (odottaa loputtomasti tai aikakatkaisun kulumista, kun tehtävä ei ole valmis) ja selvittää, onko tehtävä peruutettu vai onko se päättynyt.

Soitettava käyttöliittymä on samanlainen kuin Ajettava käyttöliittymä, koska se tarjoaa yhden menetelmän, joka kuvaa suoritettavan tehtävän. Toisin kuin Ajettavaon void run () menetelmä, Soitettavaon V-kutsu () heittää poikkeuksen method voi palauttaa arvon ja heittää poikkeuksen.

Suorittajan tehdasmenetelmät

Jossain vaiheessa haluat hankkia toimeenpanijan. Executor-kehys toimittaa Suorittajat hyödyllisyysluokka tähän tarkoitukseen. Suorittajat tarjoaa useita tehdasmenetelmiä erityyppisten suorittajien hankkimiseksi, jotka tarjoavat erityisiä ketjun suorituskäytäntöjä. Tässä on kolme esimerkkiä:

  • ExecutorService newCachedThreadPool () luo ketjupoolin, joka luo uusia ketjuja tarpeen mukaan, mutta käyttää uudelleen aiemmin rakennettuja ketjuja, kun ne ovat käytettävissä. Viestiketjut, joita ei ole käytetty 60 sekunnin ajan, lopetetaan ja poistetaan välimuistista. Tämä ketjutallennus parantaa tyypillisesti ohjelmien suorituskykyä, jotka suorittavat monia lyhytaikaisia ​​asynkronisia tehtäviä.
  • ExecutorService newSingleThreadExecutor () luo toteuttajan, joka käyttää yhtä työntekijälankaa, joka toimii rajattoman jonon ulkopuolella - tehtävät lisätään jonoon ja suoritetaan peräkkäin (korkeintaan yksi tehtävä on aktiivinen kerrallaan). Jos tämä ketju päättyy epäonnistumisen vuoksi suorituksen aikana ennen suorittimen sammuttamista, luodaan uusi ketju, joka tulee paikalleen, kun seuraavat tehtävät on suoritettava.
  • ExecutorService newFixedThreadPool (int nThreads) luo ketjupoolin, joka käyttää uudelleen kiinteää määrää säikeitä, jotka toimivat jaetun rajoittamattoman jonon ulkopuolella. Enintään nKierteet ketjut käsittelevät aktiivisesti tehtäviä. Jos uusia tehtäviä lähetetään kaikkien säikeiden ollessa aktiivisia, ne odottavat jonossa, kunnes ketju on käytettävissä. Jos jokin ketju päättyy vian vuoksi suorituksen aikana ennen sammuttamista, luodaan uusi ketju, joka tulee paikalleen, kun myöhemmät tehtävät on suoritettava. Altaan ketjut ovat olemassa, kunnes toteuttaja suljetaan.

Executor-kehys tarjoaa muita tyyppejä (kuten ScheduledExecutorService käyttöliittymä), mutta tyypit, joiden kanssa todennäköisesti työskentelet useimmiten ExecutorService, Tulevaisuus, Soitettavaja Suorittajat.

Katso java.util.concurrent Javadoc tutkia muita tyyppejä.

Työskentely Executor-kehyksen kanssa

Tulet huomaamaan, että Executor-kehystä on melko helppo käyttää. Listassa 2 olen käyttänyt Suorittaja ja Suorittajat Korvata palvelin-esimerkki luettelosta 1 skaalautuvammalla säikejoukko-pohjaisella vaihtoehdolla.

Listing 2. Server.java (versio 2)

tuo java.io.IOException; tuo java.net.ServerSocket; tuo java.net.Socket; tuo java.util.concurrent.Executor; tuo java.util.concurrent.Executors; luokka Palvelin {staattinen suorittimen varasto = Executors.newFixedThreadPool (5); public static void main (String [] args) heittää IOException {ServerSocket socket = new ServerSocket (9000); while (true) {lopullinen Socket s = socket.accept (); Runnable r = uusi Runnable () {@Override public void run () {doWork (s); }}; pool.execute (r); }} staattinen void doWork (Socket s) {}}

Listalla 2 käyttötarkoitusta newFixedThreadPool (sis.) saadaksesi ketjujoukkopohjaisen suorittajan, joka käyttää uudelleen viisi säiettä. Se myös korvaa uusi lanka (r) .start (); kanssa pool.execute (r); suoritettavien tehtävien suorittamiseen näiden ketjujen kautta.

Luettelossa 3 on toinen esimerkki, jossa sovellus lukee mielivaltaisen verkkosivun sisällön. Se antaa tuloksena olevat rivit tai virheilmoituksen, jos sisältö ei ole käytettävissä enintään viiden sekunnin kuluessa.

$config[zx-auto] not found$config[zx-overlay] not found