Programovanie

Používanie vlákien so zbierkami, 1. časť

Vlákna sú neoddeliteľnou súčasťou jazyka Java. Vďaka použitiu vlákien je prístup k mnohým algoritmom, napríklad k systémom správy front, ľahší ako k použitiu techník dotazovania a opakovania. Nedávno som pri písaní triedy Java zistil, že pri výpočte zoznamov potrebujem používať vlákna, a to odhalilo niektoré zaujímavé problémy spojené so zbierkami podporujúcimi vlákna.

Toto Java v hĺbke V stĺpci sú popísané problémy, ktoré som odhalil pri pokuse o vytvorenie kolekcie bezpečnej pre vlákna. Kolekcia sa nazýva „bezpečná pre vlákna“, keď ju môžu bezpečne používať viacerí klienti (vlákna) súčasne. „Tak v čom je problém?“ pýtaš sa. Problém je v tom, že pri bežnom používaní program mení kolekciu (tzv mutujúci) a prečíta ju (tzv vymenovanie).

Niektorí jednoducho neregistrujú vyhlásenie: „Platforma Java je viacvláknová.“ Iste, počujú to, a kývajú hlavou. Nerozumejú však tomu, že na rozdiel od C alebo C ++, v ktorých sa závitovanie priskrutkovalo zboku cez OS, sú vlákna v Jave základnými jazykovými konštruktmi. Toto nedorozumenie alebo zlé pochopenie inherentne vláknovej povahy Javy nevyhnutne vedie k dvom častým chybám v kóde Java programátorov: Buď nedokážu deklarovať synchronizovanú metódu, ktorá by mala byť (pretože objekt je v priebehu vykonanie metódy) alebo vyhlásia metódu za synchronizovanú, aby ju chránili, čo spôsobí, že zvyšok systému bude fungovať neefektívne.

Na tento problém som narazil, keď som chcel kolekciu, ktorú by mohli používať viaceré vlákna, bez zbytočného blokovania vykonávania ostatných vlákien. Žiadna z tried zbierky vo verzii 1.1 JDK nie je bezpečná pre vlákna. Konkrétne, žiadna z tried kolekcie vám neumožní počítať s jedným vláknom, zatiaľ čo mutuje s iným.

Zbierky, ktoré nie sú bezpečné pre vlákna

Môj základný problém bol nasledovný: Za predpokladu, že máte objednanú zbierku objektov, navrhnite triedu Java tak, aby vlákno mohlo vyčísliť celú alebo časť zbierky bez obáv z toho, že by sa výčet stal neplatným kvôli iným vláknam, ktoré menia kolekciu. Ako príklad problému zvážte jazyk Java Vektor trieda. Táto trieda nie je bezpečná pre vlákna a novým programátorom Java spôsobuje veľa problémov, keď ich skombinujú s viacvláknovým programom.

The Vektor trieda poskytuje veľmi užitočné zariadenie pre programátorov Java: menovite dynamicky veľké pole objektov. V praxi môžete toto zariadenie použiť na ukladanie výsledkov, kde konečný počet objektov, s ktorými budete narábať, nie je známy, kým neskončíte všetky. Na demonštráciu tohto konceptu som zostrojil nasledujúci príklad.

01 import java.util.Vector; 02 import java.util.Enumeration; 03 public class Demo {04 public static void main (String args []) {05 Vector digits = new Vector (); 06 int vysledok = 0; 07 08 if (args.length == 0) {09 System.out.println ("Používa sa ako java demo 12345"); 10 System.exit (1); 11} 12 13 pre (int i = 0; i = '0') && (c <= '9')) 16 číslic. Doplněk (nové celé číslo (c - '0')); 17 ďalších 18 prestávka; 19} 20 System.out.println ("Existujú" + digits.size () + "číslice."); 21 pre (Enumeration e = digits.elements (); e.hasMoreElements ();) {22 result = result * 10 + ((Integer) e.nextElement ()). IntValue (); 23} 24 System.out.println (args [0] + "=" + výsledok); 25 System.exit (0); 26} 27} 

Jednoduchá trieda vyššie používa a Vektor objekt na zhromažďovanie číslic z reťazca. Zbierka je potom vymenovaná tak, aby počítala celočíselnú hodnotu reťazca. S touto triedou sa nestalo nič zlé, ibaže nie je bezpečná pre vlákna. Ak sa stalo, že iné vlákno obsahuje odkaz na číslice vektor a toto vlákno vložilo do vektora nový znak, výsledky slučky v riadkoch 21 až 23 vyššie by boli nepredvídateľné. Ak k vloženiu došlo skôr, ako objekt enumerácie prešiel bodom vloženia, výpočet vlákna výsledok by spracoval nový znak. Ak by sa vloženie stalo po tom, čo enumerácia prešla bodom vloženia, slučka by znak nespracovala. Najhorším scenárom je, že slučka môže vyhodiť a NoSuchElementException ak bol narušený interný zoznam.

Tento príklad je len tento - vykonštruovaný príklad. Ukazuje to problém, ale aká je šanca na spustenie iného vlákna počas krátkeho päť- alebo šesťciferného výčtu? V tomto príklade je riziko nízke. Čas, ktorý uplynie, keď jedno vlákno spustí rizikovú operáciu, ktorou je v tomto príklade výčet, a potom dokončí úlohu, sa nazýva vlákno okno zraniteľnostialebo okno. Toto konkrétne okno je známe ako a stav rasy pretože jedno vlákno „závodí“ v dokončení svojej úlohy skôr, ako iné vlákno použije kritický zdroj (zoznam číslic). Keď však začnete používať kolekcie na reprezentáciu skupiny niekoľkých tisíc prvkov, napríklad s databázou, okno zraniteľnosti sa zvýši, pretože výčet vlákna strávi oveľa viac času vo svojej enumeračnej slučke, a to robí šancu na spustenie iného vlákna. ovela vyššie. Určite nechcete, aby nejaké iné vlákno menilo zoznam pod vami! To, čo chcete, je záruka, že Vymenovanie objekt, ktorý držíte, je platný.

Jedným zo spôsobov, ako sa na tento problém pozrieť, je poznamenať, že Vymenovanie objekt je oddelený od Vektor objekt. Pretože sú samostatné, nie sú schopní si nad sebou udržať kontrolu, akonáhle sú vytvorené. Táto voľná väzba mi naznačila, že asi užitočnou cestou k preskúmaniu bol výpočet, ktorý bol užšie zviazaný so zbierkou, ktorá ho produkovala.

Vytváranie zbierok

Aby som mohol vytvoriť svoju zbierku bezpečnú pre vlákna, potreboval som najskôr zbierku. V mojom prípade bol potrebný triedený zber, ale neobťažoval som sa ísť po celej trase binárneho stromu. Namiesto toho som vytvoril kolekciu, ktorú som nazval a SynchroList. Tento mesiac sa pozriem na základné prvky kolekcie SynchroList a popíšem, ako ju používať. Budúci mesiac v 2. časti prevediem kolekciu od jednoduchej a ľahko zrozumiteľnej triedy Java po zložitú triedu Java s viacerými vláknami. Mojím cieľom je udržať dizajn a implementáciu zbierky zreteľné a zrozumiteľné v porovnaní s technikami používanými na jej zvýšenie povedomia o vláknach.

Pomenoval som svoju triedu SynchroList. Názov „SynchroList“ samozrejme pochádza zo zreťazenia slov „synchronizácia“ a „zoznam“. Zbierka je jednoducho dvojnásobne prepojený zoznam, ktorý môžete nájsť v akejkoľvek vysokoškolskej učebnici programovania, hoci s využitím vnútornej triedy s názvom Odkaz, dá sa dosiahnuť určitá elegancia. Vnútorná trieda Odkaz je definované takto:

 trieda Odkaz {dáta súkromných objektov; private Link nxt, prv; Odkaz (objekt o, odkaz p, odkaz n) {nxt = n; prv = p; dáta = o; if (n! = null) n.prv = this; if (p! = null) p.nxt = this; } Objekt getData () {návratové údaje; } Odkaz next () {return nxt; } Link next (Link newNext) {Link r = nxt; nxt = newNext; return r;} Odkaz prev () {return prv; } Odkaz prev (Odkaz novýPrev) {Odkaz r = prv; prv = newPrev; return r;} public String toString () {return "Link (" + data + ")"; }} 

Ako vidíte v kóde vyššie, a Odkaz objekt zapuzdruje chovanie odkazov, ktoré použije zoznam na usporiadanie svojich objektov. Ak chcete implementovať správanie sa dvojnásobne prepojeného zoznamu, objekt obsahuje odkazy na jeho údajový objekt, odkaz na nasledujúci odkaz v reťazci a odkaz na predchádzajúci odkaz v reťazci. Ďalej metódy Ďalšie a prev sú preťažené, aby poskytli prostriedok na aktualizáciu ukazovateľa objektu. Je to nevyhnutné, pretože nadradená trieda bude musieť do zoznamu vkladať a mazať odkazy. Konštruktor odkazu je navrhnutý na vytvorenie a vloženie odkazu súčasne. Toto ušetrí volanie metódy pri implementácii zoznamu.

V zozname sa používa iná vnútorná trieda - v tomto prípade trieda enumerátora s názvom ListEnumerator. Táto trieda implementuje java.util.Enumerácia rozhranie: štandardný mechanizmus, ktorý Java používa na iteráciu zbierky objektov. Tým, že náš enumerátor implementuje toto rozhranie, bude naša kolekcia kompatibilná s akýmikoľvek inými triedami Java, ktoré používajú toto rozhranie na výčet obsahu kolekcie. Implementácia tejto triedy je uvedená v kóde nižšie.

 trieda LinkEnumerator implementuje Enumeration {private Link current, previous; LinkEnumerator () {current = head; } public boolean hasMoreElements () {return (current! = null); } public Object nextElement () {Object result = null; Link tmp; if (current! = null) {result = current.getData (); current = current.next (); } vrátiť výsledok; }} 

Vo svojej súčasnej inkarnácii LinkEnumerator trieda je dosť priama; zmenením sa to skomplikuje. V tejto inkarnácii jednoducho prechádza zoznamom volajúceho objektu, kým nepríde k poslednému odkazu v internom prepojenom zozname. Tieto dve metódy potrebné na implementáciu java.util.Enumerácia rozhrania sú hasMoreElements a nextElement.

Jedným z dôvodov, prečo nepoužívame java.util.Vector triedy, pretože som potreboval zoradiť hodnoty v zbierke. Mali sme na výber: zostaviť túto kolekciu tak, aby bola konkrétna pre konkrétny typ objektu, a tak sa na jeho triedenie využila dôverná znalosť typu objektu, alebo aby sa vytvorilo všeobecnejšie riešenie založené na rozhraniach. Zvolil som druhú metódu a definoval som rozhranie s názvom Komparátor na zapuzdrenie metód potrebných na triedenie objektov. Toto rozhranie je zobrazené nižšie.

 public interface Comparator {public boolean lessThan (Object a, Object b); public boolean greaterThan (Objekt a, Objekt b); public boolean equalTo (Objekt a, Objekt b); void typeCheck (Objekt a); } 

Ako vidíte vo vyššie uvedenom kóde, Komparátor rozhranie je veľmi jednoduché. Rozhranie vyžaduje jednu metódu pre každú z troch základných porovnávacích operácií. Pomocou tohto rozhrania môže zoznam porovnávať pridané alebo odstránené objekty s objektmi, ktoré sa už na zozname nachádzajú. Konečná metóda, typ skontrolovať, sa používa na zaistenie typovej bezpečnosti zbierky. Keď Komparátor objekt sa používa, Komparátor možno použiť na zabezpečenie toho, aby boli všetky objekty v kolekcii rovnakého typu. Hodnota tejto kontroly typu je, že vám ušetrí nevidenie výnimiek na vrhanie objektov, ak objekt v zozname nebol typu, aký ste očakávali. Neskôr som dostal príklad, ktorý používa a Komparátor, ale skôr ako sa dostaneme k príkladu, pozrime sa na SynchroList triedy priamo.

 verejná trieda SynchroList {trieda Link {... toto bolo zobrazené vyššie ...} trieda LinkEnumerator implementuje Enumeration {... trieda enumerátora ...} / * Objekt na porovnanie našich prvkov * / Comparator cmp; Spojovacia hlava, chvost; public SynchroList () {} public SynchroList (komparátor c) {cmp = c; } private void before (Object o, Link p) {new Link (o, p.prev (), p); } private void after (Object o, Link p) {new Link (o, p, p.next ()); } private void remove (Odkaz p) {if (p.prev () == null) {head = p.next (); (p.next ()). prev (null); } else if (p.next () == null) {tail = p.prev (); (p.prev ()). next (null); } else {p.prev (). next (p.next ()); p.next (). prev (p.prev ()); }} public void add (Objekt o) {// ak má cmp hodnotu null, vždy pridá na koniec zoznamu. if (cmp == null) {if (head == null) {head = new Link (o, null, null); chvost = hlava; } else {tail = new Link (o, tail, null); } návrat; } cmp.typeCheck (o); if (head == null) {head = new Link (o, null, null); chvost = hlava; } else if (cmp.lessThan (o, head.getData ())) {head = new Link (o, null, head); } else {Link l; for (l = head; l.next ()! = null; l = l.next ()) {if (cmp.lessThan (o, l.getData ())) {before (o, l); návrat; }} chvost = nový odkaz (o, chvost, null); } návrat; } public boolean delete (Objekt o) {if (cmp == null) return false; cmp.typeCheck (o); for (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); návrat pravdivý; } if (cmp.lessThan (o, l.getData ())) break; } return false; } verejné synchronizované Enumeration elements () {návrat nového LinkEnumerator (); } public int size () {int vysledok = 0; pre (odkaz l = hlava; l! = null; l = l.next ()) výsledok ++; návratový výsledok; }} 
$config[zx-auto] not found$config[zx-overlay] not found