Programmering

Java 101: Java-samtidighed uden smerte, del 1

Med den stigende kompleksitet af samtidige applikationer finder mange udviklere, at Java's lave trådningsfunktioner på lavt niveau er utilstrækkelige til deres programmeringsbehov. I så fald kan det være tid til at opdage Java Concurrency Utilities. Kom godt i gang med java.util.concurrent, med Jeff Friesens detaljerede introduktion til Executor-rammen, synkroniseringstyper og Java Concurrent Collections-pakken.

Java 101: Den næste generation

Den første artikel i denne nye JavaWorld-serie introducerer Java Date and Time API.

Java-platformen leverer threading-funktioner på lavt niveau, der gør det muligt for udviklere at skrive samtidige applikationer, hvor forskellige tråde udføres samtidigt. Standard Java-threading har dog nogle ulemper:

  • Java's lavt niveau samtidige primitiver (synkroniseret, flygtige, vente(), underrette()og notifyAll ()) er ikke nemme at bruge korrekt. Trådningsfarer som dødvande, trådsult og raceforhold, som skyldes forkert brug af primitiver, er også svære at opdage og fejle.
  • Stole på synkroniseret at koordinere adgang mellem tråde fører til ydeevneproblemer, der påvirker applikationsskalerbarhed, et krav for mange moderne applikationer.
  • Java's grundlæggende threading-funktioner er også lavt niveau. Udviklere har ofte brug for konstruktioner på højere niveau som semaforer og trådpuljer, som Java's lavt niveau-trådningsfunktioner ikke tilbyder. Som et resultat vil udviklere bygge deres egne konstruktioner, hvilket både er tidskrævende og fejlbehæftet.

JSR 166: Concurrency Utilities-rammen blev designet til at imødekomme behovet for et trådniveau på højt niveau. Initieret i begyndelsen af ​​2002 blev rammen formaliseret og implementeret to år senere i Java 5. Forbedringer har fulgt i Java 6, Java 7 og den kommende Java 8.

Denne todelte Java 101: Den næste generation serien introducerer softwareudviklere, der er fortrolige med grundlæggende Java-threading til Java Concurrency Utilities-pakker og rammer. I del 1 præsenterer jeg en oversigt over rammen om Java Concurrency Utilities og introducerer dens Executor-ramme, synkroniseringsværktøjer og Java Concurrent Collections-pakken.

Forståelse af Java-tråde

Inden du dykker ind i denne serie, skal du sørge for at være fortrolig med det grundlæggende i trådning. Start med Java 101 introduktion til Java's lavt niveau threading kapaciteter:

  • Del 1: Introduktion af tråde og løbere
  • Del 2: Trådsynkronisering
  • Del 3: Trådplanlægning, vent / underret og trådafbrydelse
  • Del 4: Trådgrupper, volatilitet, tråd-lokale variabler, timere og tråddød

Inde i Java Concurrency Utilities

Java Concurrency Utilities-rammen er et bibliotek af typer der er designet til at blive brugt som byggesten til oprettelse af samtidige klasser eller applikationer. Disse typer er trådsikre, er grundigt testet og tilbyder høj ydeevne.

Typer i Java Concurrency Utilities er organiseret i små rammer; nemlig Executor framework, synchronizer, samtidige samlinger, låse, atomvariabler og Fork / Join. De er yderligere organiseret i en hovedpakke og et par underpakker:

  • java.util.concurrent indeholder værktøjstyper på højt niveau, der ofte bruges i samtidig programmering. Eksempler inkluderer semaforer, barrierer, trådpuljer og samtidige hashmaps.
    • Det java.util.concurrent.atomic underpakke indeholder værktøjsklasser på lavt niveau, der understøtter låsefri trådsikker programmering på enkelte variabler.
    • Det java.util.concurrent.locks underpakke indeholder hjælpeprogrammer på lavt niveau til at låse og vente på forhold, der adskiller sig fra at bruge Java's lavt niveau synkronisering og skærme.

Java Concurrency Utilities-rammen udsætter også lavt niveau sammenligne og bytte (CAS) hardwareinstruktion, hvis varianter ofte understøttes af moderne processorer. CAS er meget mere let end Java's monitorbaserede synkroniseringsmekanisme og bruges til at implementere nogle meget skalerbare samtidige klasser. CAS-baseret java.util.concurrent.locks.ReentrantLock klasse er for eksempel mere performant end den tilsvarende skærmbaserede synkroniseret primitiv. ReentrantLock giver mere kontrol over låsning. (I del 2 forklarer jeg mere om, hvordan CAS fungerer i java.util.concurrent.)

System.nanoTime ()

Java Concurrency Utilities-rammen inkluderer lang nanoTime (), som er medlem af java.lang.System klasse. Denne metode giver adgang til en nanosekund-granularitet-tidskilde til at foretage relative tidsmålinger.

I de næste afsnit introducerer jeg tre nyttige funktioner i Java Concurrency Utilities, hvor jeg først forklarer, hvorfor de er så vigtige for moderne samtidighed og derefter demonstrerer, hvordan de arbejder for at øge hastigheden, pålideligheden, effektiviteten og skalerbarheden af ​​samtidige Java-applikationer.

Executor-rammen

I trådning er en opgave er en enhed af arbejde. Et problem med trådning på lavt niveau i Java er, at indsendelse af opgave er tæt forbundet med en opgaveudførelsespolitik, som det fremgår af liste 1.

Liste 1. Server.java (version 1)

importere java.io.IOException; importere java.net.ServerSocket; import java.net.Socket; klasse Server {offentlig statisk ugyldig hoved (String [] args) kaster IOException {ServerSocket-sokkel = ny ServerSocket (9000); while (true) {final Socket s = socket.accept (); Runnable r = new Runnable () {@Override public void run () {doWork (s); }}; ny tråd (r) .start (); }} statisk ugyldigt doWork (stikkontakter) {}}

Ovenstående kode beskriver en simpel serverapplikation (med doWork (stikkontakt) efterladt tom for kortfattethed). Servertråden kalder gentagne gange socket.accept () at vente på en indgående anmodning og derefter starte en tråd til at servicere denne anmodning, når den ankommer.

Da denne applikation opretter en ny tråd for hver anmodning, skaleres den ikke godt, når den står over for et stort antal anmodninger. For eksempel kræver hver oprettet tråd hukommelse, og for mange tråde kan udtømme den tilgængelige hukommelse, hvilket tvinger applikationen til at afslutte.

Du kan løse dette problem ved at ændre politik til opgaveudførelse. I stedet for altid at oprette en ny tråd, kan du bruge en trådpulje, hvor et fast antal tråde vil betjene indgående opgaver. Du bliver dog nødt til at omskrive applikationen for at foretage denne ændring.

java.util.concurrent inkluderer Executor-rammen, en lille ramme af typer, der afkobler opgaveindgivelse fra opgaveudførelsespolitikker. Ved hjælp af Executor-rammen er det nemt at indstille et programs opgaveudførelsespolitik uden at skulle omskrive din kode markant.

Inde i Executor-rammen

Executor-rammen er baseret på Eksekutor interface, der beskriver en eksekutor som ethvert objekt, der er i stand til at udføre java.lang.Køres opgaver. Denne grænseflade erklærer følgende ensomme metode til udførelse af en Kan køres opgave:

void execute (Runnable command)

Du indsender en Kan køres opgave ved at videregive den til udføre (Runnable). Hvis eksekutøren af ​​en eller anden grund ikke kan udføre opgaven (for eksempel hvis eksekutøren er lukket ned), kaster denne metode et RejectedExecutionException.

Nøglebegrebet er det opgaveindgivelse afkobles fra opgaveudførelsespolitikken, som er beskrevet af en Eksekutor implementering. Det kører opgave er således i stand til at udføre via en ny tråd, en samlet tråd, den opkaldende tråd osv.

Noter det Eksekutor er meget begrænset. For eksempel kan du ikke lukke en eksekutor ned eller afgøre, om en asynkron opgave er afsluttet. Du kan heller ikke annullere en kørende opgave. Af disse og andre grunde giver Executor-rammen en ExecutorService-grænseflade, der udvides Eksekutor.

Fem af ExecutorServiceMetoder er især bemærkelsesværdige:

  • boolean awaitTermination (lang timeout, TimeUnit enhed) blokerer opkaldstråden, indtil alle opgaver er færdiggjort efter en nedlukningsanmodning, timeout opstår, eller den aktuelle tråd afbrydes, alt efter hvad der sker først. Den maksimale ventetid er specificeret af tiden er gået, og denne værdi udtrykkes i enhed enheder specificeret af TimeUnit enum; for eksempel, TimeUnit.SECONDS. Denne metode kaster java.lang.InterruptedException når den aktuelle tråd afbrydes. Det vender tilbage rigtigt når eksekutøren afsluttes og falsk når timeoutet løber inden opsigelse.
  • boolsk isShutdown () vender tilbage rigtigt når eksekutøren er lukket ned.
  • ugyldig nedlukning () initierer en ordnet nedlukning, hvor tidligere indsendte opgaver udføres, men ingen nye opgaver accepteres.
  • Fremtidig indsendelse (kaldbar opgave) sender en værdi-retur opgave til udførelse og returnerer en Fremtid repræsenterer de afventende resultater af opgaven.
  • Fremtidig indsendelse (Kørbar opgave) indsender en Kan køres opgave til udførelse og returnering a Fremtid repræsenterer denne opgave.

Det Fremtid interface repræsenterer resultatet af en asynkron beregning. Resultatet er kendt som en fremtid fordi det typisk ikke vil være tilgængeligt før et øjeblik i fremtiden. Du kan påberåbe dig metoder til at annullere en opgave, returnere en opgaves resultat (venter på ubestemt tid eller indtil en timeout udløber, når opgaven ikke er færdig) og bestemme, om en opgave er blevet annulleret eller er afsluttet.

Det Kan kaldes interface ligner Kan køres interface ved, at det giver en enkelt metode, der beskriver en opgave, der skal udføres. I modsætning til Kan køres's ugyldig kørsel () metode, Kan kaldes's V-opkald () kaster undtagelse metode kan returnere en værdi og kaste en undtagelse.

Executor fabriksmetoder

På et eller andet tidspunkt vil du have en eksekutor. Executor-rammen leverer Eksekutører utility klasse til dette formål. Eksekutører tilbyder flere fabriksmetoder til at opnå forskellige slags eksekutører, der tilbyder specifikke trådudførelsespolitikker. Her er tre eksempler:

  • ExecutorService newCachedThreadPool () opretter en trådpulje, der opretter nye tråde efter behov, men som genbruger tidligere konstruerede tråde, når de er tilgængelige. Tråde, der ikke har været brugt i 60 sekunder, afsluttes og fjernes fra cachen. Denne trådpulje forbedrer typisk ydeevnen for programmer, der udfører mange kortvarige asynkrone opgaver.
  • ExecutorService newSingleThreadExecutor () opretter en eksekutor, der bruger en enkelt arbejdstråd, der styrer en ubegrænset kø - opgaver føjes til køen og udføres sekventielt (ikke mere end én opgave er aktiv ad gangen). Hvis denne tråd afsluttes ved en fejl under udførelsen inden lukning af eksekutoren, oprettes en ny tråd, der tager plads, når efterfølgende opgaver skal udføres.
  • ExecutorService newFixedThreadPool (int nThreads) opretter en trådpulje, der genbruger et fast antal tråde, der fungerer fra en delt ubegrænset kø. Højst nTråde tråde behandler aktivt opgaver. Hvis der indsendes yderligere opgaver, når alle tråde er aktive, venter de i køen, indtil en tråd er tilgængelig. Hvis en tråd afsluttes ved fejl under udførelse før nedlukning, oprettes en ny tråd, der træder i stedet, når efterfølgende opgaver skal udføres. Puljens tråde eksisterer, indtil eksekutøren lukkes ned.

Executor-rammen tilbyder yderligere typer (såsom ScheduledExecutorService interface), men de typer, du sandsynligvis vil arbejde med oftest, er ExecutorService, Fremtid, Kan kaldesog Eksekutører.

Se java.util.concurrent Javadoc for at udforske yderligere typer.

Arbejde med Executor-rammen

Du finder ud af, at Executor-rammen er ret let at arbejde med. I liste 2 har jeg brugt Eksekutor og Eksekutører at erstatte servereksemplet fra liste 1 med et mere skalerbart trådbaseret alternativ.

Liste 2. Server.java (version 2)

importere java.io.IOException; importere java.net.ServerSocket; import java.net.Socket; importere java.util.concurrent.Executor; importere java.util.concurrent.Executors; klasse Server {statisk Executor-pool = Executors.newFixedThreadPool (5); offentlig statisk ugyldig hoved (String [] args) kaster IOException {ServerSocket-sokkel = ny ServerSocket (9000); while (true) {final Socket s = socket.accept (); Runnable r = new Runnable () {@Override public void run () {doWork (s); }}; pool.execute (r); }} statisk ugyldigt doWork (stikkontakter) {}}

Listing 2 anvendelser newFixedThreadPool (int) for at få en trådbaseret eksekutor, der genbruger fem tråde. Det erstatter også ny tråd (r) .start (); med pool.execute (r); til udførelse af kørbare opgaver via en af ​​disse tråde.

Liste 3 viser et andet eksempel, hvor en applikation læser indholdet af en vilkårlig webside. Det udsender de resulterende linjer eller en fejlmeddelelse, hvis indholdet ikke er tilgængeligt inden for maksimalt fem sekunder.