En guide til samtidige køer i Java

1. Oversikt

I denne opplæringen går vi gjennom noen av hovedimplementeringene av samtidige køer i Java. For en generell introduksjon til køer, se vår guide til Java Grensesnittartikkel.

2. Køer

I flertrådede applikasjoner må køene håndtere flere samtidige produsent-forbrukerscenarier. Riktig valg av samtidig kø kan være avgjørende for å oppnå god ytelse i algoritmene våre.

For det første ser vi noen viktige forskjeller mellom en blokkerende kø og en ikke-blokkerende. Deretter tar vi en titt på noen implementeringer og beste praksis.

2. Blokkering mot ikke-blokkerende kø

BlockingQueue tilbud en enkel trådsikker mekanisme. I denne køen må tråder vente på at køen er tilgjengelig. Produsentene vil vente på ledig kapasitet før de legger til elementer, mens forbrukerne vil vente til køen er tom. I slike tilfeller vil den ikke-blokkerende køen enten kaste et unntak eller returnere en spesiell verdi, som null eller falsk.

For å oppnå denne blokkeringsmekanismen, BlockingQueue grensesnittet viser to funksjoner på toppen av det normale funksjoner: sette og ta. Disse funksjonene tilsvarer legge til og fjerne i en standard .

3. Samtidig Implementeringer

3.1. ArrayBlockingQueue

Som navnet antyder, bruker denne køen en matrise internt. Som en konsekvens er det en avgrenset kø, noe som betyr at den har en fast størrelse.

En enkel arbeidskø er et eksempel på bruk. Dette scenariet er ofte et lavt forhold mellom produsent og forbruker, hvor vi deler tidkrevende oppgaver på flere arbeidstakere. Siden denne køen ikke kan vokse på ubestemt tid, størrelsesgrensen fungerer som en sikkerhetsterskel hvis minne er et problem.

Når det gjelder minne, er det viktig å merke seg at køen forhåndsallokerer matrisen. Selv om dette kan forbedre gjennomstrømningen, er det kan også forbruke mer minne enn nødvendig. For eksempel kan en kø med stor kapasitet være tom i lange perioder.

Også, den ArrayBlockingQueue bruker en enkelt lås for begge sette og ta operasjoner. Dette sikrer ingen overskriving av bidrag, på bekostning av en forestilling.

3.2. LinkedBlockingQueue

De LinkedBlockingQueue bruker en LinkedList variant, der hvert køelement er en ny node. Selv om dette i utgangspunktet gjør køen ubegrenset, har den fortsatt en hard grense på Heltall.MAX_VALUE.

På den annen side kan vi stille inn køstørrelsen ved å bruke konstruktøren LinkedBlockingQueue (int kapasitet).

Denne køen bruker forskjellige låser for sette og ta operasjoner. Som en konsekvens kan begge operasjonene gjøres parallelt og forbedre gjennomstrømningen.

Siden LinkedBlockingQueue kan være enten avgrenset eller ubegrenset, hvorfor bruker vi ArrayBlockingQueue over denne? LinkedBlockingQueue trenger å tildele og distribuere noder hver gang et element blir lagt til eller fjernet fra køen. Av denne grunn an ArrayBlockingQueue kan være et bedre alternativ hvis køen vokser raskt og krymper raskt.

Utførelsen av LinkedBlockingQueue sies å være uforutsigbar. Med andre ord, vi trenger alltid å profilere våre scenarier for å sikre at vi bruker riktig datastruktur.

3.3. PriorityBlockingQueue

De PriorityBlockingQueue er vår gå-til-løsning når vi trenger å konsumere varer i en bestemt rekkefølge. For å oppnå dette, PriorityBlockingQueue bruker en array-basert binær bunke.

Mens det internt bruker en enkelt låsemekanisme, er ta operasjonen kan skje samtidig med sette operasjon. Bruk av en enkel spinlock gjør dette mulig.

En typisk brukssak er å konsumere oppgaver med forskjellige prioriteringer. Vi ønsker ikke at en lavprioritetsoppgave skal innta en høyt prioritert oppgave.

3.4. DelayQueue

Vi bruker en DelayQueuenår en forbruker bare kan ta en utgått vare. Interessant, det bruker en PriorityQueue internt for å bestille varene etter utløpet.

Siden dette ikke er en generell kø, dekker den ikke så mange scenarier som ArrayBlockingQueue eller LinkedBlockingQueue. For eksempel kan vi bruke denne køen til å implementere en enkel hendelsessløyfe som ligner på det som finnes i NodeJS. Vi plasserer asynkrone oppgaver i køen for senere behandling når de utløper.

3.5. LinkedTransferQueue

De LinkedTransferQueue introduserer en overføre metode. Mens andre køer vanligvis blokkeres når de produserer eller forbruker varer, blir LinkedTransferQueuetillater en produsent å vente på forbruket av en vare.

Vi bruker en LinkedTransferQueue når vi trenger en garanti for at en bestemt vare vi setter i køen er tatt av noen. Vi kan også implementere en enkel mottrykksalgoritme ved hjelp av denne køen. Ved å blokkere produsenter til forbruk, forbrukere kan drive strømmen av meldinger som produseres.

3.6. SynchronousQueue

Mens køer vanligvis inneholder mange gjenstander, er SynchronousQueue vil alltid ha, på det meste, et enkelt element. Med andre ord, vi trenger å se SynchronousQueue som en enkel måte å utveksle data mellom to tråder.

Når vi har to tråder som trenger tilgang til en delt tilstand, synkroniserer vi disse ofte med CountDownLatch eller andre synkroniseringsmekanismer. Ved å bruke en SynchronousQueue, vi kan unngå denne manuelle synkroniseringen av tråder.

3.7. ConcurrentLinkedQueue

De ConcurrentLinkedQueue er den eneste ikke-blokkerende køen i denne guiden. Derfor gir den en "ventfri" algoritme hvor legge til og avstemming er garantert trådsikre og kommer tilbake umiddelbart. I stedet for låser bruker denne køen CAS (Compare-And-Swap).

Internt er det basert på en algoritme fra Enkle, raske og praktiske ikke-blokkerende og blokkerende samtidige køalgoritmer av Maged M. Michael og Michael L. Scott.

Det er en perfekt kandidat for moderne reaktive systemerhvor bruk av blokkering av datastrukturer ofte er forbudt.

På den annen side, hvis forbrukeren vår ender med å vente i en løkke, bør vi sannsynligvis velge en blokkerende kø som et bedre alternativ.

4. Konklusjon

I denne guiden gikk vi gjennom forskjellige samtidige køimplementeringer, og diskuterte deres styrker og svakheter. Med dette i bakhodet er vi bedre rustet til å utvikle effektive, holdbare og tilgjengelige systemer.


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