Programmering

Brug af tråde med samlinger, del 1

Tråde er en integreret del af Java-sproget. Ved hjælp af tråde er mange algoritmer, såsom køadministrationssystemer, lettere tilgængelige end de bruger polling- og looping-teknikker. For nylig, mens jeg skrev en Java-klasse, fandt jeg, at jeg var nødt til at bruge tråde, mens jeg opregnede lister, og dette afslørede nogle interessante problemer forbundet med trådbevidste samlinger.

Det her Java dybt kolonne beskriver de problemer, jeg afslørede i mit forsøg på at udvikle en trådsikker samling. En samling kaldes "trådsikker", når den kan bruges sikkert af flere klienter (tråde) på samme tid. "Så hvad er problemet?" du spørger. Problemet er, at i typisk brug ændrer et program begge en samling (kaldet mutere) og læser det (kaldet optælling).

Nogle mennesker registrerer simpelthen ikke udsagnet "Java-platformen er multitrådet." Visst, de hører det, og de nikker på hovedet. Men de forstår ikke, at i modsætning til C eller C ++, hvor trådning blev boltet på fra siden gennem operativsystemet, er tråde i Java grundlæggende sprogkonstruktioner. Denne misforståelse eller dårlig forståelse af Java's iboende gevindskarakter fører uundgåeligt til to almindelige fejl i programmørernes Java-kode: Enten undlader de at erklære en metode som synkroniseret, der burde være (fordi objektet er i en inkonsekvent tilstand under metodens udførelse) eller de erklærer en metode som synkroniseret for at beskytte den, hvilket får resten af ​​systemet til at fungere ineffektivt.

Jeg stødte på dette problem, da jeg ønskede en samling, som flere tråde kunne bruge uden unødigt at blokere udførelsen af ​​de andre tråde. Ingen af ​​samlingsklasser i 1.1-versionen af ​​JDK er trådsikre. Specifikt giver ingen af ​​samlingsklasser dig mulighed for at tælle med en tråd, mens du muterer med en anden.

Ikke-trådsikre samlinger

Mit grundlæggende problem var som følger: Forudsat at du har en ordnet samling af objekter, skal du designe en Java-klasse, så en tråd kan tælle hele eller en del af samlingen uden at bekymre dig om, at tællingen bliver ugyldig på grund af andre tråde, der ændrer samlingen. Overvej Java'er som et eksempel på problemet Vektor klasse. Denne klasse er ikke trådsikker og forårsager mange problemer for nye Java-programmører, når de kombinerer den med et multitrådet program.

Det Vektor klasse giver en meget nyttig facilitet for Java-programmører: nemlig en dynamisk størrelse matrix af objekter. I praksis kan du muligvis bruge denne facilitet til at gemme resultater, hvor det endelige antal objekter, du vil beskæftige dig med, ikke er kendt, før du er færdig med dem alle. Jeg konstruerede følgende eksempel for at demonstrere dette koncept.

01 import java.util.Vector; 02 import java.util.Enumeration; 03 offentlig klasse demo {04 offentlig statisk ugyldig hoved (String args []) {05 Vector cifre = ny Vector (); 06 int resultat = 0; 07 08 hvis (args.length == 0) {09 System.out.println ("Brug er java demo 12345"); 10 System.exit (1); 11} 12 13 for (int i = 0; i = '0') && (c <= '9')) 16 cifre.addElement (nyt heltal (c - '0')); 17 andet 18 pause; 19} 20 System.out.println ("Der er" + cifre. Størrelse () + "cifre."); 21 for (Tælling e = cifre.elementer (); e.hasMoreElements ();) {22 resultat = resultat * 10 + ((Heltal) e.nextElement ()). IntValue (); 23} 24 System.out.println (args [0] + "=" + resultat); 25 System.exit (0); 26} 27} 

Den enkle klasse ovenfor bruger a Vektor objekt til at indsamle cifrede tegn fra en streng. Samlingen tælles derefter for at beregne strengets heltalsværdi. Der er ikke noget galt med denne klasse, bortset fra at den ikke er trådsikker. Hvis en anden tråd tilfældigvis indeholdt en henvisning til cifre vektor, og den tråd indsatte et nyt tegn i vektoren, ville resultaterne af sløjfen i linierne 21 til 23 ovenfor være uforudsigelige. Hvis indsættelsen fandt sted, før optællingsobjektet havde passeret indsættelsespunktet, beregner tråden resultat ville behandle den nye karakter. Hvis indsættelsen skete, efter at optællingen havde passeret indsættelsespunktet, ville sløjfen ikke behandle tegnet. Det værst tænkelige scenario er, at løkken måske kaster et NoSuchElementException hvis den interne liste var kompromitteret.

Dette eksempel er netop det - et konstrueret eksempel. Det demonstrerer problemet, men hvad er chancen for, at en anden tråd kører under en kort fem- eller sekscifret optælling? I dette eksempel er risikoen lav. Den tid, der går, når en tråd starter en operation i fare, hvilket i dette eksempel er optællingen, og derefter afslutter opgaven kaldes trådens vindue med sårbarhed, eller vindue. Dette særlige vindue er kendt som en race tilstand fordi en tråd "kører" for at afslutte sin opgave, før en anden tråd bruger den kritiske ressource (listen med cifre). Når du begynder at bruge samlinger til at repræsentere en gruppe på flere tusinde elementer, som f.eks. Med en database, øges sårbarhedsvinduet, fordi trådoptællingen bruger meget mere tid på dens optællingsløkke, og det gør chancen for, at en anden tråd kører meget højere. Du vil bestemt ikke have en anden tråd, der ændrer listen under dig! Hvad du ønsker er en forsikring om, at Optælling det objekt, du har, er gyldigt.

En måde at se på dette problem er at bemærke, at Optælling objekt er adskilt fra Vektor objekt. Fordi de er adskilte, er de ikke i stand til at bevare kontrollen over hinanden, når de først er oprettet. Denne løse binding foreslog mig, at måske en nyttig vej til at udforske var en optælling, der var tættere bundet til den samling, der producerede den.

Oprettelse af samlinger

For at oprette min trådsikre samling havde jeg først brug for en samling. I mit tilfælde var der brug for en sorteret samling, men jeg gider ikke at gå den fulde binære trærute. I stedet oprettede jeg en samling, som jeg kaldte en SynchroList. Denne måned vil jeg se på kerneelementerne i SynchroList-samlingen og beskrive, hvordan man bruger den. Næste måned, i del 2, tager jeg samlingen fra en enkel, letforståelig Java-klasse til en kompleks flertrådet Java-klasse. Mit mål er at holde design og implementering af en samling tydelig og forståelig i forhold til de teknikker, der bruges til at gøre den trådbevidst.

Jeg navngav min klasse SynchroList. Navnet "SynchroList" kommer selvfølgelig fra sammenkædningen af ​​"synkronisering" og "liste". Samlingen er simpelthen en dobbeltkoblet liste, som du måske finder i enhver college-lærebog om programmering, dog gennem brug af en indre klasse med navnet Link, en vis elegance kan opnås. Den indre klasse Link er defineret som følger:

 klasse Link {private objektdata; privat Link nxt, prv; Link (Objekt o, Link p, Link n) {nxt = n; prv = p; data = o; hvis (n! = null) n.prv = dette; hvis (p! = null) p.nxt = dette; } Objekt getData () {returner data; } Link næste () {return nxt; } Link næste (Link newNext) {Link r = nxt; nxt = newNext; return r;} Link prev () {return prv; } Link tidligere (Link newPrev) {Link r = prv; prv = newPrev; returner r;} offentlig String toString () {return "Link (" + data + ")"; }} 

Som du kan se i koden ovenfor, a Link objekt indkapsler den sammenkædningsadfærd, som listen vil bruge til at organisere sine objekter. For at implementere den dobbeltkoblede listeopførsel indeholder objektet referencer til dets dataobjekt, en henvisning til det næste link i kæden og en henvisning til det forrige link i kæden. Yderligere metoderne Næste og tidligere er overbelastet for at give et middel til at opdatere objektets markør. Dette er nødvendigt, da den overordnede klasse skal indsætte og slette links på listen. Linkkonstruktøren er designet til at oprette og indsætte et link på samme tid. Dette gemmer et metodekald i implementeringen af ​​listen.

En anden indre klasse bruges på listen - i dette tilfælde en tællerklasse ved navn ListEnumerator. Denne klasse implementerer java.util.Enumeration interface: den standardmekanisme, som Java bruger til at gentage over en samling objekter. Ved at lade vores tæller implementere denne grænseflade, vil vores samling være kompatibel med andre Java-klasser, der bruger denne grænseflade til at tælle indholdet af en samling. Implementeringen af ​​denne klasse vises i nedenstående kode.

 klasse LinkEnumerator implementerer Enumeration {private Link nuværende, forrige; LinkEnumerator () {nuværende = hoved; } offentlige boolske hasMoreElements () {return (nuværende! = null); } offentligt objekt nextElement () {Objektresultat = null; Link tmp; hvis (nuværende! = null) {resultat = nuværende.getData (); strøm = strøm.næste (); } returnere resultat }} 

I sin nuværende inkarnation har den LinkEnumerator klasse er ret ligetil; det bliver mere kompliceret, når vi ændrer det. I denne inkarnation går den simpelthen gennem listen for det kaldende objekt, indtil det kommer til det sidste link på den interne linkede liste. De to metoder, der kræves for at implementere java.util.Enumeration interface er hasMoreElements og nextElement.

Selvfølgelig er en af ​​grundene til, at vi ikke bruger java.util.Vector klasse er fordi jeg havde brug for at sortere værdierne i samlingen. Vi havde et valg: at opbygge denne samling til at være specifik for en bestemt type objekt og derved bruge den intime viden om objekttypen til at sortere den eller skabe en mere generisk løsning baseret på grænseflader. Jeg valgte den sidstnævnte metode og definerede en navngivet grænseflade Komparator at indkapsle de nødvendige metoder til at sortere objekter. Denne grænseflade er vist nedenfor.

 offentlig grænseflade Comparator {public boolean lessThan (Object a, Object b); offentlig boolsk størreThan (Objekt a, Objekt b); offentlig boolsk equalTo (Objekt a, Objekt b); ugyldig typeCheck (Objekt a); } 

Som du kan se i ovenstående kode, er Komparator interface er ret simpelt. Interfacet kræver en metode til hver af de tre grundlæggende sammenligningsoperationer. Ved hjælp af denne grænseflade kan listen sammenligne de objekter, der tilføjes eller fjernes med objekter, der allerede er på listen. Den endelige metode, typeKontrol, bruges til at sikre samlingens typesikkerhed. Når Komparator objekt bruges, Komparator kan bruges til at sikre, at objekter i samlingen alle er af samme type. Værdien af ​​denne typekontrol er, at det sparer dig for at se undtagelser fra objekt casting, hvis objektet på listen ikke var af den type, du forventede. Jeg har et eksempel senere, der bruger en Komparator, men inden vi kommer til eksemplet, lad os se på SynchroList klasse direkte.

 offentlig klasse SynchroList {class Link {... dette blev vist ovenfor ...} class LinkEnumerator implementerer Enumeration {... the enumerator class ...} / * Et objekt til sammenligning af vores elementer * / Comparator cmp; Link hoved, hale; offentlig SynchroList () {} offentlig SynchroList (Comparator c) {cmp = c; } privat ugyldigt før (Objekt o, Link p) {nyt Link (o, p.prev (), p); } privat tomrum efter (Objekt o, Link p) {nyt Link (o, p, s.næste ()); } privat tomrum fjern (Link p) {if (p.prev () == null) {head = p.next (); (s.next ()). prev (null); } ellers hvis (p.next () == null) {tail = p.prev (); (s. forrige ()). næste (null); } andet {p.prev (). næste (p.next ()); p.next (). prev (p.prev ()); }} public void add (Object o) {// hvis cmp er null, skal du altid tilføje til halen på listen. if (cmp == null) {if (head == null) {head = new Link (o, null, null); hale = hoved; } ellers {tail = new Link (o, tail, null); } Vend tilbage; } cmp.typeCheck (o); if (head == null) {head = new Link (o, null, null); hale = hoved; } ellers hvis (cmp.lessThan (o, head.getData ())) {head = new Link (o, null, head); } andet {Link l; for (l = head; l.next ()! = null; l = l.next ()) {if (cmp.lessThan (o, l.getData ())) {før (o, l); Vend tilbage; }} hale = nyt link (o, hale, null); } Vend tilbage; } offentlig boolsk sletning (Objekt o) {hvis (cmp == null) returnerer falsk; cmp.typeCheck (o); for (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); returner sandt; } hvis (cmp.lessThan (o, l.getData ())) bryde; } returner falsk } offentlige synkroniserede optællingselementer () {returner ny LinkEnumerator (); } offentlig int-størrelse () {int-resultat = 0; for (Link l = head; l! = null; l = l.next ()) result ++; returresultat }}