Ring Buffer megvalósítása Java-ban

1. Áttekintés

Ebben az oktatóanyagban megtanuljuk, hogyan kell megvalósítani a gyűrűpuffert Java-ban.

2. Ring puffer

A Ring Buffer (vagy Circular Buffer) egy korlátozott kör alakú adatstruktúra, amelyet két vagy több szál közötti adatok pufferelésére használnak. Amint folyamatosan írunk egy gyűrűs pufferre, a végére érve körbetekerik.

2.1. Hogyan működik

A gyűrűpuffert egy fix méretű tömb felhasználásával valósítják meg, amely körbefut a határokon.

A tömbön kívül három dolgot nyomon követ:

  • a következő elérhető rés a pufferben egy elem beillesztésére,
  • a puffer következő olvasatlan eleme,
  • és a tömb vége - az a pont, ahol a puffer körbeteker a tömb elejéig

A gyűrűs puffer e követelmények kezelésének mechanikája a megvalósítástól függ. Például a témával kapcsolatos Wikipedia-bejegyzés négymutatót alkalmazó módszert mutat.

A megközelítést Disruptor gyűrűpuffer szekvenciák segítségével történő megvalósításából kölcsönözzük.

Az első dolog, amit tudnunk kell, a kapacitás - a puffer rögzített maximális mérete. Következő, két monoton növekedést fogunk használniszekvenciák:

  • Írja a szekvenciát: -1-től kezdődően 1-gyel növekszik, amikor beillesztünk egy elemet
  • Olvassa el a Szekvencia: 0-tól kezdődően 1-gyel növekszik, amikor egy elemet fogyasztunk

A tömb indexéhez leképezhetünk egy szekvenciát egy mod művelet segítségével:

arrayIndex = sorozat% kapacitás 

A A mod művelet a határok köré tekeri a sorrendet, hogy rést nyerjen a pufferben:

Nézzük meg, hogyan illesztenénk be egy elemet:

puffer [++ writeSequence% capacity] = elem 

Egy elem beszúrása előtt előre növeljük a szekvenciát.

Egy elem fogyasztásához utólagos növekedést hajtunk végre:

elem = puffer [readSequence ++% kapacitás] 

Ebben az esetben utólagos növekedést hajtunk végre a szekvencián. Egy elem fogyasztása nem távolítja el a pufferből - csak addig marad a tömbben, amíg felül nem íródik.

2.2. Üres és teljes pufferek

Amint körbetekerjük a tömböt, elkezdjük felülírni az adatokat a pufferben. Ha a puffer megtelt, akkor választhatjuk, hogy felülírjuk-e a legrégebbi adatokat, függetlenül attól, hogy az olvasó elfogyasztotta-e, vagy megakadályozhatjuk az elolvasatlan adatok felülírását.

Ha az olvasó megengedheti magának, hogy kihagyja a köztes vagy a régi értékeket (például egy részvényárfolyam-ticker), akkor felülírhatjuk az adatokat, anélkül, hogy megvárnánk a felhasználást. Másrészt, ha az olvasónak el kell fogyasztania az összes értéket (mint például az e-kereskedelmi tranzakcióknál), akkor várnunk kell (blokk / foglalt-várakozás), amíg a pufferben rendelkezésre áll egy rés.

A puffer megtelt, ha a puffer mérete megegyezik a kapacitásával, ahol a mérete megegyezik az olvasatlan elemek számával:

méret = (writeSequence - readSequence) + 1 isFull = (méret == kapacitás) 

Ha az írási szekvencia elmarad az olvasási szekvenciától, akkor a puffer üres:

isEmpty = writeSequence <readSequence 

A puffer visszaadja a nulla érték, ha üres.

2.2. Előnyök és hátrányok

A gyűrűpuffer hatékony FIFO puffer. Rögzített méretű tömböt használ, amelyet előre el lehet osztani, és hatékony memóriaelérési mintát tesz lehetővé. Az összes puffer művelet állandó idő O (1), beleértve egy elem fogyasztását, mivel ez nem igényli az elemek váltását.

A másik oldalon kritikus a gyűrűpuffer helyes méretének meghatározása. Például az írási műveletek hosszú ideig blokkolhatnak, ha a puffer alulméretezett és az olvasás lassú. Használhatunk dinamikus méretezést, de ehhez adatok mozgatására lenne szükség, és a fent tárgyalt előnyök nagy részét kihagyjuk.

3. Megvalósítás Java-ban

Most, hogy megértettük a gyűrűpuffer működését, folytassuk a Java alkalmazásban.

3.1. Inicializálás

Először definiáljunk egy konstruktort, amely inicializálja a puffert egy előre meghatározott kapacitással:

public CircularBuffer (int kapacitás) {this.kapacitás = kapacitás; this.data = (E []) új objektum [kapacitás]; this.readSequence = 0; this.writeSequence = -1; } 

Ez létrehoz egy üres puffert és inicializálja a szekvenciamezőket az előző szakaszban leírtak szerint.

3.3. Ajánlat

Ezután megvalósítjuk a ajánlat művelet, amely a következő elérhető résbe beilleszt egy elemet a pufferbe, és visszatér igaz a sikerről. Visszatér hamis ha a puffer nem talál üres rést, vagyis nem írhatjuk felül az olvasatlan értékeket.

Végezzük el a ajánlat módszer Java-ban:

nyilvános logikai ajánlat (E elem) {logikai isFull = (writeSequence - readSequence) + 1 == kapacitás; if (! isFull) {int nextWriteSeq = writeSequence + 1; adatok [nextWriteSeq% kapacitás] = elem; writeSequence ++; return true; } return false; } 

Tehát növeljük az írási sorrendet, és kiszámoljuk a tömb indexét a következő elérhető réshez. Ezután írjuk az adatokat a pufferbe, és tároljuk a frissített írási sorrendet.

Próbáljuk ki:

@Test public void givenCircularBuffer_whenAnElementIsEnqueued_thenSizeIsOne () {CircularBuffer buffer = new CircularBuffer (defaultCapacity); assertTrue (buffer.offer ("Négyzet")); assertEquals (1, buffer.size ()); } 

3.4. Közvélemény kutatás

Végül megvalósítjuk a közvélemény kutatás művelet, amely lekéri és eltávolítja a következő olvasatlan elemet. A közvélemény kutatás A művelet nem távolítja el az elemet, hanem növeli az olvasási sorrendet.

Végezzük el:

nyilvános E poll () {logikai isEmpty = writeSequence <readSequence; if (! isEmpty) {E nextValue = adatok [readSequence% kapacitás]; readSequence ++; return nextValue; } return null; } 

Itt az aktuális olvasási sorrend adatait olvassuk a tömb indexének kiszámításával. Ezután növeljük a sorrendet és visszaadjuk az értéket, ha a puffer nem üres.

Próbáljuk ki:

@Test public void givenCircularBuffer_whenAnElementIsDequeued_thenElementMatchesEnqueuedElement () {CircularBuffer buffer = new CircularBuffer (defaultCapacity); buffer.offer ("Háromszög"); Karakterlánc alakja = buffer.poll (); assertEquals ("háromszög", alak); } 

4. Termelő-fogyasztó probléma

Beszéltünk egy gyűrűs puffer használatáról két vagy több szál közötti adatcserére, amely példa egy szinkronizálási problémára, amelyet Producer-Consumer problémának hívnak. A Java-ban a termelő-fogyasztó problémáját különféle módokon oldhatjuk meg szemaforok, korlátozott sorok, gyűrűpufferek stb.

Vezessünk be egy gyűrűpufferen alapuló megoldást.

4.1. illó Szekvencia mezők

A gyűrűs puffer megvalósítása nem biztonságos a menetben. Tegyük szálbiztonságossá az egyszerű egytermelő és egyfogyasztó esetében.

A gyártó adatokat ír a pufferbe, és növeli a writeSequence, míg a fogyasztó csak a pufferből olvas és növeli a readSequence. Tehát, a háttértömb nem vitatott és szinkronizálás nélkül megúszhatjuk.

De még mindig biztosítanunk kell, hogy a fogyasztó lássa a legfrissebb értéket writeSequence mező (láthatóság), és hogy a writeSequence nem frissül, mielőtt az adatok ténylegesen rendelkezésre állnak a pufferben (megrendelés).

A gyűrűpuffert egyidejűvé és zármentessé tehetjük ebben az esetben a szekvenciamezők létrehozásával illó:

private volatile int writeSequence = -1, readSequence = 0; 

Ban,-ben ajánlat módszer, írjon a illó terület writeSequence garantálja, hogy az írások a pufferbe a szekvencia frissítése előtt történnek. Ugyanakkor a illó a láthatósági garancia biztosítja, hogy a fogyasztó mindig a legújabb értékét lássa writeSequence.

4.2. Termelő

Vezessünk be egy egyszerű producert Futható amely a gyűrűpufferbe írja:

public void run () {for (int i = 0; i <item.length;) {if (buffer.offer (items [i])) {System.out.println ("Előállítva:" + elemek [i]) ; i ++; }}} 

A producer szál egy üres rést várna egy hurokban (foglalt-várakozás).

4.3. Fogyasztó

Megvalósítjuk a fogyasztót Hívható amely a pufferből olvasható:

nyilvános T [] hívás () {T [] tételek = (T []) új objektum [várható szám]; for (int i = 0; i <tételek.hossz;) {T elem = puffer.poll (); if (elem! = null) {item [i ++] = item; System.out.println ("Elfogyasztva:" + elem); }} visszaküldendő tételek; } 

A fogyasztói szál nyomtatás nélkül folytatódik, ha a nulla érték a pufferből.

Írjuk meg a vezető kódunkat:

végrehajtóService.submit (új szál (új Producer (puffer))); végrehajtóService.submit (új szál (új fogyasztó (puffer))); 

Termelő-fogyasztó programunk végrehajtása az alábbiakhoz hasonló eredményt hoz létre:

Gyártva: Kör Gyártva: Háromszög elfogyasztva: Gyártott téglalap: Fogyasztott téglalap: Elfogyasztott téglalap: Téglalap gyártva: Szögletes Gyártva: Elfogyasztott rombusz: Szögletes Gyártva: Trapéz elfogyasztva: Rombusz elfogyasztva: Trapéz előállítva: Pentagon Gyártva: Pentagram Gyártva: Hatszög fogyasztva: Pent Pentagram előállítva: Hexagram fogyasztva: Hatszög fogyasztva: Hexagram 

5. Következtetés

Ebben az oktatóanyagban megtanultuk, hogyan kell megvalósítani a gyűrűpuffert, és feltártuk, hogyan lehet a gyártó-fogyasztó problémáját megoldani.

Szokás szerint az összes példa forráskódja elérhető a GitHubon.