Programmering

JavaBeans: egenskaber, begivenheder og trådsikkerhed

Java er et dynamisk sprog, der inkluderer brugervenlige multithreading-sprogkonstruktioner og supportklasser. Mange Java-programmer er afhængige af multitrådning for at udnytte intern applikationsparallelisme, forbedre netværksydelse eller fremskynde feedback fra brugerne. De fleste Java-kørselstider bruger multithreading til at implementere Java's funktion til affaldssamling. Endelig er AWT også afhængig af separate tråde for dets funktion. Kort sagt, selv de enkleste af Java-programmer er født i et aktivt multithreading-miljø.

Java bønner er derfor også implementeret i et sådant dynamisk, multithread miljø, og heri ligger den klassiske fare for at støde på race betingelser. Raceforhold er tidsafhængige programflowscenarier, der kan føre til tilstandskorruption (programdata). I det følgende afsnit beskriver jeg to sådanne scenarier. Hver Java-bønne skal designes med race-forhold i tankerne, så en bønne kan modstå samtidig brug af flere klienttråde.

Multithreading-problemer med enkle egenskaber

Bønneimplementeringer skal antage, at flere tråde får adgang til og / eller ændrer en enkelt bønneinstans på samme tid. Som et eksempel på en ukorrekt implementeret bønne (som det vedrører multithreading bevidsthed), overvej følgende BrokenProperties bønne og dens tilknyttede MTProperties testprogram:

BrokenProperties.java

importere java.awt.Point;

// Demo Bean, der ikke beskytter mod brug af flere tråde.

offentlig klasse BrokenProperties udvider punkt {

// ------------------------------------------------ ------------------- // set () / get () for 'Spot' egenskab // --------------- -------------------------------------------------- -

public void setSpot (Point point) {// 'spot' setter this.x = point.x; this.y = point.y;

} public Point getSpot () {// 'spot' getter returnerer dette; }} // Afslutning på Bean / Class BrokenProperties

MTPegenskaber.java

importere java.awt.Point; importværktøjer. *; importværktøjer. bønner. *;

offentlig klasse MTProperties udvider tråd {

beskyttede BrokenProperties myBean; // målbønnen at bash ..

beskyttet int myID; // hver tråd bærer lidt ID

// ------------------------------------------------ ------------------- // hoved () indgangspunkt // ---------------------- --------------------------------------------- offentlig statisk ugyldig hoved ( Streng [] args) {

BrokenProperties bønne; Tråd tråd;

bønne = (BrokenProperties) BeansKit.newBean ("BrokenProperties");

for (int i = 0; i <20; i ++) {// start 20 tråde for at bash bønnetråd = nye MTP-egenskaber (bean, i); // tråde får adgang til bønnetråd.start (); }} // ------------------------------------------------------ --------------------- // MTProperties Constructor // ----------------------- --------------------------------------------

offentlige MTProperties (BrokenProperties bean, int id) {this.myBean = bean; // bemærk bønnen for at adressere dette. myID = id; // bemærk hvem vi er} // ----------------------------------------- -------------------------- // trådens hovedsløjfe: // gør for evigt // opret nyt tilfældigt punkt med x == y // fortæl bønne at vedtage punkt som sin nye "spot" -egenskab // spørg bean hvad dens "spot" -egenskab nu er indstillet til // kaste en wobbly hvis spot x ikke svarer til spot y // --------- -------------------------------------------------- -------- public void run () {int someInt; Punktpunkt = nyt punkt ();

while (true) {someInt = (int) (Math.random () * 100); point.x = nogetInt; point.y = nogetInt; myBean.setSpot (punkt);

point = myBean.getSpot (); hvis (point.x! = point.y) {System.out.println ("Bønne ødelagt! x =" + point.x + ", y =" + point.y); System.exit (10); } System.out.print ((char) ('A' + myID)); System.out.flush (); }}} // Afslutning af klasse MTP-ejendomme

Bemærk: Værktøjspakken importeret af MTP-ejendomme indeholder genanvendelige klasser og statiske metoder udviklet til bogen af ​​forfatteren.

De to kildekodelister ovenfor definerer en bønne kaldet BrokenProperties og klassen MTP-ejendomme, som bruges til at udøve bønnen inden for 20 løbende tråde. Lad os følge med MTP-ejendomme' hoved () indgangspunkt: Først instantierer det en BrokenProperties bønne, efterfulgt af oprettelse og start af 20 tråde. Klasse MTP-ejendomme strækker sig java.lang.Tråd, så alt hvad vi skal gøre for at vende klasse MTP-ejendomme i en tråd er at tilsidesætte klassen Tråd's løb() metode. Konstruktøren til vores tråde tager to argumenter: det bønneobjekt, tråden kommunikerer med, og en unik identifikation, der gør det muligt at differentiere de 20 tråde let ved kørsel.

Afslutningen på denne demo er vores løb() metode i klassen MTP-ejendomme. Her sløjfer vi for evigt og skaber tilfældige nye (x, y) punkter, men med følgende egenskab: deres x-koordinat er altid lig med deres y-koordinat. Disse tilfældige punkter videregives til bønnerne setSpot () setter-metoden og derefter straks læse tilbage ved hjælp af getSpot () getter-metoden. Du ville forvente læsningen få øje på egenskab for at være identisk med det tilfældige punkt, der blev oprettet for nogle millisekunder siden. Her er et eksempel på output af programmet, når det påberåbes på kommandolinjen:

ABBBBBBBBBBBBBBBBBBDJJJJJJJJJJJJJJJJJJJJEGHHHHHHHHHHHHHHHHHHSSSSSSSSSSSSSS IICCBBBBBBBBBBBBBBBBBKBDLJMBOPLQNRTPHHHHHHHHFFFFFFFFFFFFSSSSSSSSSSSSSSSSSS FFFFFFFFFFFFFFFAAAAAACCCCCCCKKKKKKKKKKKKKKKKKMMMMMMMMMMMMMMMMMMMMMMMMMMMDD JEOQQQQQQQQQQQQQQQRRRRRRRRRRRRRRRRRBBBBBBBBBBBBBBBTTTTTTTTTTTTTTTTLPPPPPPP PPPPGGHHHFFFFFFFFIIIIIIIIIIIIIISSSSSSSSSSSSSSSSSSSSACCCCCCCCCCCCCCCCCCCKMD QQQQQQNNNNNNNNNNNNNNNNRRRRRTRRHHHHHHHHHFFFFFFFFFFFFFFFFFFIIIIIIIIIIIIIIIII MMMJEEEEEEEEEEDDDDEEEEEEEEEOOOOOOOOOOOOOOOOOOOOOOOOOOOQNNNNNNNNBTLPLRGFFFF FFFFFFFFFIIAAAAAAAAAAAAAAAAASSSSSSSSSSSSSSSSSSKKKKKKKKKKKKKKKKCCCCCCCMMJAA AACBean ødelagt! x = 67, y = 13 OOOOOOOOOOOOOOOOOOO 

Outputtet viser de 20 tråde, der kører parallelt (så vidt en menneskelig observatør går); hver tråd bruger det id, de modtog på konstruktionstidspunktet til at udskrive et af bogstaverne EN til T, de første 20 bogstaver i alfabetet. Så snart enhver tråd opdager, at læsningen er tilbage få øje på egenskab er ikke i overensstemmelse med den programmerede karakteristik af x = y, tråden udskriver en "Bean corrupted" besked og stopper eksperimentet.

Hvad du ser er den statskorrupterende bivirkning af en race tilstand inden for bønnens setSpot () kode. Her er den metode igen:

public void setSpot (Point point) {// 'spot' setter this.x = point.x; this.y = point.y; } 

Hvad kunne der nogensinde gå galt i en så simpel kode? Forestille tråd A ringer setSpot () med et punktargument svarende til (67,67). Hvis vi nu sænker universets ur, så vi kan se Java virtual machine (JVM) udføre hver Java-sætning, en ad gangen, kan vi forestille os tråd A udførelse af x-koordinatkopi-sætningen (this.x = point.x;) og så pludselig tråd A bliver frosset af operativsystemet, og tråd C er planlagt til at køre et stykke tid. I sin tidligere kørende tilstand tråd C havde netop oprettet sit eget nye tilfældige punkt (13,13), kaldet setSpot () sig selv og blev derefter frossen for at gøre plads til tråd M, lige efter at den havde indstillet x-koordinaten til 13. Så genoptages tråd C fortsætter nu med sin programmerede logik: indstilling af y til 13 og kontrol af, om spot-ejendommen er lige (13, 13), men finder ud af, at den på mystisk vis har ændret sig til en ulovlig tilstand af (67, 13); x-koordinaten er halvdelen af ​​hvad tråd A var ved indstilling få øje på til, og y-koordinaten er halvdelen af ​​hvad tråd Chavde satfå øje på til. Slutresultatet er, at BrokenProperties bønne ender med en intern inkonsekvent tilstand: en brudt ejendom.

Hver gang en ikke-atomisk datastruktur (dvs. en struktur bestående af mere end en del) kan ændres af mere end en tråd ad gangen, du skal beskytte strukturen ved hjælp af en lås. I Java gøres dette ved hjælp af synkroniseret nøgleord.

Advarsel: I modsætning til alle andre Java-typer skal du bemærke, at Java ikke garanterer det lang og dobbelt behandles atomisk! Dette er fordi lang og dobbelt kræver 64 bits, hvilket er dobbelt så lang som de fleste moderne CPU-arkitekturs ordlængde (32 bit). Både indlæsning og lagring af enkelte maskineord er iboende atomare operationer, men at flytte 64-bit enheder kræver to sådanne træk, og disse er ikke beskyttet af Java af den sædvanlige årsag: ydeevne. (Nogle CPU'er tillader systembussen at blive låst til at udføre multiordoverførsler atomare, men denne facilitet er ikke tilgængelig på alle CPU'er og vil under alle omstændigheder være utrolig dyr at anvende på alle lang eller dobbelt manipulationer!) Så selv når en ejendom kun består af en enkelt lang eller en enkelt dobbelt, skal du bruge fuld låseforholdsregler for at beskytte dine længsler eller fordoblinger fra pludselig at blive totalt ødelagt.

Det synkroniseret nøgleord markerer en blok kode som værende et atomært trin. Koden kan ikke "opdeles", som når en anden tråd afbryder koden for potentielt at genindføre den blokering selv (deraf udtrykket genindgangskode; al Java-kode skal genindlæses). Løsningen til vores BrokenProperties bønne er triviel: bare udskift den setSpot () metode med denne:

public void setSpot (Point point) {// 'spot' setter synkroniseret (dette) {this.x = point.x; this.y = point.y; }} 

Eller alternativt med dette:

offentlig synkroniseret ugyldig setSpot (Point point) {// 'spot' setter this.x = point.x; this.y = point.y; } 

Begge udskiftninger er helt ækvivalente, selvom jeg foretrækker den første stil, fordi den viser mere præcist, hvad den nøjagtige funktion af synkroniseret nøgleord er: en synkroniseret blok er altid knyttet til et objekt, der bliver låst. Ved Låst Jeg mener, at JVM først forsøger at opnå en lås (dvs. eksklusiv adgang) på objektet (dvs. opnå eksklusiv adgang til det) eller venter, indtil objektet bliver låst op, hvis det er blevet låst af en anden tråd. Låseprocessen garanterer, at ethvert objekt kun kan låses (eller ejes) af en tråd ad gangen.

synkroniseret (dette) syntaksen gentager klart den interne mekanisme: Argumentet inden for parenteser er det objekt, der skal låses (det aktuelle objekt), inden kodeblokken indtastes. Den alternative syntaks, hvor synkroniseret nøgleord bruges som en modifikator i en metodesignatur, er simpelthen en stenografisk version af den tidligere.

Advarsel: Når statiske metoder er markeret synkroniseret, der er ingen det her objekt til at låse; kun instansmetoder er knyttet til et aktuelt objekt. Så når klassemetoder er synkroniseret, java.lang.Klasse objekt, der svarer til metodens klasse, bruges i stedet til at låse på. Denne tilgang har alvorlige ydeevneimplikationer, fordi en samling af klasseinstanser deler en enkelt tilknyttet Klasse objekt; når som helst Klasse objekt bliver låst, alle objekter af den klasse (hvad enten det er 3, 50 eller 1000!) er udelukket fra at påberåbe sig den samme statiske metode. Med dette i tankerne skal du tænke to gange, før du bruger synkronisering med statiske metoder.

I praksis skal du altid huske den eksplicitte synkroniserede form, fordi den giver dig mulighed for at "forstøve" den mindst mulige blok af kode inden for en metode. Forkortelsesformen "forstøver" hele metoden, som af præstationsårsager ofte er ikke hvad du vil have. Når en tråd er kommet ind i en atomblok af kode, er der ingen anden tråd, der skal udføres nogen synkroniseret kode på det samme objekt kan gøre det.

Tip: Når der opnås en lås på en genstand, så alle synkroniseret kode for det objekts klasse bliver atomær. Derfor, hvis din klasse indeholder mere end en datastruktur, der skal behandles atomare, men disse datastrukturer er ellers uafhængig af hinanden, så kan der opstå en ny præstationsflaskehals. Kunder, der kalder på synkroniserede metoder, der manipulerer en intern datastruktur, blokerer alle andre klienter, der kalder de andre metoder, der beskæftiger sig med andre atomare datastrukturer i din klasse. Det er klart, at du bør undgå sådanne situationer ved at opdele klassen i mindre klasser, der kun håndterer en datastruktur, der skal behandles atomisk ad gangen.

JVM implementerer sin synkroniseringsfunktion ved at oprette køer af tråde, der venter på, at et objekt låses op. Selvom denne strategi er god, når det kommer til at beskytte konsistensen af ​​sammensatte datastrukturer, kan den resultere i multitråde trafikpropper, når en mindre end effektiv sektion af kode er markeret som synkroniseret.

Vær derfor altid opmærksom på, hvor meget kode du synkroniserer: det skal være det absolut nødvendige minimum. Forestil dig f.eks. Vores setSpot () metoden oprindeligt bestod af:

public void setSpot (Point point) {// 'spot' setter log.println ("setSpot () kaldte" + this.toString ()); this.x = point.x; this.y = point.y; } 

Selvom println udsagn måske logisk hører hjemme i setSpot () metode, er det ikke en del af udsagnssekvensen, der skal grupperes i en atomhele. Derfor, i dette tilfælde, den rigtige måde at bruge synkroniseret nøgleord ville være som følger:

public void setSpot (Point point) {// 'spot' setter log.println ("setSpot () kaldte" + this.toString ()); synkroniseret (dette) {this.x = point.x; this.y = point.y; }} 

Den "dovne" måde og den tilgang, du skal undgå, ser sådan ud: