Programmering

Programmering af Java-tråde i den virkelige verden, del 1

Alle andre Java-programmer end enkle konsolbaserede applikationer er multitrådede, uanset om du kan lide det eller ej. Problemet er, at Abstract Windowing Toolkit (AWT) behandler operativsystemhændelser (OS) på sin egen tråd, så dine lyttermetoder rent faktisk kører på AWT-tråden. De samme lyttermetoder har typisk adgang til objekter, der også er adgang til fra hovedtråden. Det kan på dette tidspunkt være fristende at begrave dit hoved i sandet og lade som om du ikke behøver at bekymre dig om trådspørgsmål, men du kan normalt ikke komme væk med det. Og desværre behandler næsten ingen af ​​Java-bøgerne trådspørgsmål i tilstrækkelig dybde. (Se Ressourcer for en liste over nyttige bøger om emnet.)

Denne artikel er den første i en serie, der vil præsentere virkelige løsninger på problemerne med programmering af Java i et multitrådet miljø. Det er rettet mod Java-programmører, der forstår ting på sprogniveau ( synkroniseret nøgleord og de forskellige faciliteter i Tråd klasse), men ønsker at lære, hvordan man bruger disse sprogfunktioner effektivt.

Platformafhængighed

Desværre falder Javas løfte om platformuafhængighed fladt på ansigtet i trådarenaen. Selvom det er muligt at skrive et platformuafhængigt multitrådet Java-program, skal du gøre det med åbne øjne. Dette er ikke rigtig Java's skyld; det er næsten umuligt at skrive et virkelig platformuafhængigt trådsystem. (Doug Schmidts ACE [Adaptive Communication Environment] -ramme er et godt, selvom det er komplekst, forsøg. Se ressourcer for et link til hans program.) Så før jeg kan tale om hard-core Java-programmeringsproblemer i efterfølgende tranche, skal jeg diskutere de vanskeligheder, der er indført med de platforme, som den virtuelle Java-maskine (JVM) kan køre på.

Atomenergi

Det første koncept på OS-niveau, der er vigtigt at forstå, er atomicitet. En atomoperation kan ikke afbrydes af en anden tråd. Java definerer i det mindste nogle få atomoperationer. Især tildeling til variabler af enhver type undtagen lang eller dobbelt er atomisk. Du behøver ikke bekymre dig om en tråd, der forhindrer en metode midt i opgaven. I praksis betyder det, at du aldrig behøver at synkronisere en metode, der ikke gør andet end at returnere værdien af ​​(eller tildele en værdi til) a boolsk eller int instansvariabel. Tilsvarende behøver en metode, der udførte meget beregning ved kun at bruge lokale variabler og argumenter, og som tildelte resultaterne af denne beregning til en instansvariabel som det sidste, den gjorde, ikke at blive synkroniseret. For eksempel:

klasse noget_klasse {int noget_ felt; ugyldigt f (nogle_klasse arg) // bevidst ikke synkroniseret {// Gør mange ting her, der bruger lokale variabler // og metode argumenter, men ikke får adgang til // nogen felter i klassen (eller kalder nogen metoder // der får adgang til nogen felter i klassen). // ... noget_felt = nyt_værdi; // gør dette sidst. }} 

På den anden side, når du udfører x = ++ y eller x + = y, du kunne blive forudbestemt efter forøgelsen, men før opgaven. For at få atomicitet i denne situation skal du bruge nøgleordet synkroniseret.

Alt dette er vigtigt, fordi synkroniseringsomkostningerne kan være ikke-private og kan variere fra OS til OS. Følgende program viser problemet. Hver sløjfe kalder gentagne gange på en metode, der udfører de samme operationer, men en af ​​metoderne (låsning ()) er synkroniseret og den anden (ikke_låsning ()) er ikke. Ved hjælp af JDK "performance-pack" VM, der kører under Windows NT 4, rapporterer programmet en forskel på 1,2 sekund i runtime mellem de to sløjfer eller ca. 1,2 mikrosekunder pr. Opkald. Denne forskel ser måske ikke ud til at være meget, men den repræsenterer en stigning på 7,25 procent i opkaldstid. Selvfølgelig falder den procentvise stigning, da metoden gør mere arbejde, men et betydeligt antal metoder - i det mindste i mine programmer - er kun få linier kode.

importer java.util. *; klassesynkronisering {  synkroniseret int-låsning (int a, int b) {return a + b;} int not_locking (int a, int b) {return a + b;}  privat statisk endelig IT ITERATIONS = 1000000; statisk offentligt tomrum hoved (String [] args) {synch tester = new synch (); dobbelt start = ny dato (). getTime ();  i (lang i = ITERATIONS; --i> = 0;) tester.locking (0,0);  dobbelt slut = ny dato (). getTime (); dobbelt låsningstid = slut - start; start = ny dato (). getTime ();  i (lang i = ITERATIONS; --i> = 0;) tester. ikke_låsning (0,0);  slut = ny dato (). getTime (); dobbelt not_locking_time = slut - start; dobbelt time_in_synchronization = locking_time - not_locking_time; System.out.println ("Tid tabt til synkronisering (millis.):" + Time_in_synchronization); System.out.println ("Låsning af omkostninger pr. Opkald:" + (time_in_synchronization / ITERATIONS)); System.out.println (not_locking_time / locking_time * 100.0 + "% stigning"); }} 

Selvom HotSpot VM formodes at løse problemet med synkronisering-overhead, er HotSpot ikke en freebee - du skal købe den. Medmindre du licenserer og sender HotSpot med din app, fortæller du ikke, hvad VM vil være på målplatformen, og selvfølgelig vil du have så lidt som muligt af eksekveringshastigheden for dit program at være afhængig af den VM, der udfører det. Selvom deadlock-problemer (som jeg vil diskutere i den næste del af denne serie) ikke eksisterede, er forestillingen om, at du skal "synkronisere alt", simpelthen forkert med hovedet.

Samtidighed versus parallelisme

Det næste OS-relaterede problem (og det største problem, når det kommer til at skrive platform-uafhængig Java) har at gøre med forestillingerne om samtidighed og parallelisme. Samtidige multithreading-systemer giver udseendet af flere opgaver, der udføres på én gang, men disse opgaver er faktisk opdelt i klumper, der deler processoren med klumper fra andre opgaver. Følgende figur illustrerer problemerne. I parallelle systemer udføres to opgaver faktisk samtidigt. Parallelisme kræver et multiple-CPU-system.

Medmindre du bruger meget tid blokeret og venter på, at I / O-operationer er færdige, vil et program, der bruger flere samtidige tråde, ofte køre langsommere end et ækvivalent enkelttrådsprogram, selvom det ofte vil være bedre organiseret end den tilsvarende single -tråd version. Et program, der bruger flere tråde, der kører parallelt på flere processorer, kører meget hurtigere.

Selvom Java tillader, at threading implementeres fuldstændigt i VM, i det mindste i teorien, vil denne tilgang udelukke enhver parallelisme i din applikation. Hvis der ikke blev brugt tråde på operativsystemniveau, ville operativsystemet se på VM-forekomsten som en applikation med en enkelt gevind, som sandsynligvis ville være planlagt til en enkelt processor. Nettoresultatet ville være, at ingen to Java-tråde, der kører under den samme VM-instans, nogensinde ville køre parallelt, selvom du havde flere CPU'er, og din VM var den eneste aktive proces. To forekomster af VM, der kører separate applikationer, kan naturligvis køre parallelt, men jeg vil gøre det bedre end det. For at få parallelisme er VM skal kortlæg Java-tråde igennem til OS-tråde; så du har ikke råd til at ignorere forskellene mellem de forskellige trådmodeller, hvis platformuafhængighed er vigtig.

Få dine prioriteter lige

Jeg demonstrerer, hvordan de problemer, jeg lige har diskuteret, kan påvirke dine programmer ved at sammenligne to operativsystemer: Solaris og Windows NT.

Java, i det mindste i teorien, giver ti prioritetsniveauer for tråde. (Hvis to eller flere tråde begge venter på at køre, udføres den med det højeste prioritetsniveau.) I Solaris, som understøtter 231 prioritetsniveauer, er dette ikke noget problem (selvom Solaris-prioriteter kan være vanskelige at bruge - mere om dette om et øjeblik). NT har derimod syv prioritetsniveauer til rådighed, og disse skal kortlægges i Java's ti. Denne kortlægning er udefineret, så der er mange muligheder. (For eksempel kan Java-prioritetsniveauer 1 og 2 begge kortlægges til NT-prioritetsniveau 1, og Java-prioritetsniveauer 8, 9 og 10 kan alle kortlægges til NT-niveau 7.)

NT's mangel på prioritetsniveauer er et problem, hvis du vil bruge prioritet til at kontrollere planlægning. Ting gøres endnu mere komplicerede af, at prioritetsniveauer ikke er faste. NT tilvejebringer en mekanisme kaldet prioritetsforøgelse, som du kan slå fra med et C-systemopkald, men ikke fra Java. Når prioritetsforøgelse er aktiveret, øger NT en tråds prioritet med et ubestemt beløb i en ubestemt tid hver gang det udfører visse I / O-relaterede systemopkald. I praksis betyder det, at en tråds prioritetsniveau kan være højere, end du tror, ​​fordi den tråd tilfældigvis udførte en I / O-operation på et akavet tidspunkt.

Pointen med den prioriterede boost er at forhindre tråde, der udfører baggrundsbehandling, at påvirke den tilsyneladende lydhørhed af UI-tunge opgaver. Andre operativsystemer har mere sofistikerede algoritmer, der typisk sænker prioriteten for baggrundsprocesser. Ulempen ved denne ordning, især når den implementeres på et per-thread snarere end et per-proces-niveau, er, at det er meget vanskeligt at bruge prioritet til at bestemme, hvornår en bestemt tråd skal køre.

Det bliver værre.

I Solaris, som det er tilfældet i alle Unix-systemer, har processer såvel prioritet som tråde. Trådene i processer med høj prioritet kan ikke afbrydes af trådene i processer med lav prioritet. Desuden kan prioritetsniveauet for en given proces begrænses af en systemadministrator, så en brugerproces ikke afbryder kritiske OS-processer. NT støtter intet af dette. En NT-proces er bare et adresseområde. Det har ingen prioritet i sig selv og er ikke planlagt. Systemet planlægger tråde; derefter, hvis en given tråd kører under en proces, der ikke er i hukommelsen, byttes processen ind. NT-trådprioriteter falder i forskellige "prioritetsklasser", der fordeles over et kontinuum af faktiske prioriteter. Systemet ser sådan ud:

Kolonnerne er faktiske prioritetsniveauer, hvoraf kun 22 skal deles af alle applikationer. (De andre bruges af NT selv.) Rækkerne er prioritetsklasser. Trådene, der kører i en proces, der er knyttet til ledig prioritetsklasse, kører på niveau 1 til 6 og 15 afhængigt af deres tildelte logiske prioritetsniveau. Trådene i en proces, der er knyttet som normal prioritetsklasse, kører på niveau 1, 6 til 10 eller 15, hvis processen ikke har inputfokus. Hvis det har inputfokus, køres trådene på niveau 1, 7 til 11 eller 15. Dette betyder, at en højprioritetstråd i en ledig prioritetsklasseproces kan forhindre en tråd med lav prioritet i en normal prioritetsklasseproces, men kun hvis denne proces kører i baggrunden. Bemærk, at en proces, der kører i "høj" prioritetsklasse, kun har seks prioritetsniveauer til rådighed. De andre klasser har syv.

NT giver ingen måde at begrænse prioritetsklassen for en proces. Enhver tråd på enhver proces på maskinen kan til enhver tid overtage kontrollen over kassen ved at øge sin egen prioritetsklasse; der er intet forsvar mod dette.

Det tekniske udtryk, jeg bruger til at beskrive NTs prioritet, er uhellig rod. I praksis er prioritet næsten værdiløs under NT.

Så hvad skal en programmør gøre? Mellem NTs begrænsede antal prioritetsniveauer, og det er ukontrollerbar prioritetsforøgelse, er der ingen absolut sikker måde for et Java-program at bruge prioritetsniveauer til planlægning. Et brugbart kompromis er at begrænse dig til Tråd.MAX_PRIORITY, Tråd.MIN_PRIORITYog Tråd.NORM_PRIORITY når du ringer setPriority (). Denne begrænsning undgår i det mindste problemet med 10 niveauer-kortlagt til 7 niveauer. Jeg formoder, du kunne bruge os.navn systemegenskab for at opdage NT og derefter kalde en oprindelig metode for at slå prioritetsforøgelse fra, men det fungerer ikke, hvis din app kører under Internet Explorer, medmindre du også bruger Suns VM-plug-in. (Microsofts VM bruger en ikke-standard implementering af native-metoden.) Under alle omstændigheder hader jeg at bruge native-metoder. Jeg undgår normalt problemet så meget som muligt ved at sætte de fleste tråde på NORM_PRIORITY og ved hjælp af andre planlægningsmekanismer end prioritet. (Jeg vil diskutere nogle af disse i fremtidige rater af denne serie.)

Samarbejde!

Der er typisk to threading-modeller understøttet af operativsystemer: samarbejdsvillige og præemptive.

Den kooperative multithreading-model

I en samarbejdsvillig system, beholder en tråd kontrol over sin processor, indtil den beslutter at opgive den (hvilket måske aldrig bliver). De forskellige tråde er nødt til at samarbejde med hinanden, eller alle tråde undtagen en vil blive "sultet" (hvilket betyder, aldrig givet en chance for at løbe). Planlægning i de fleste samarbejdssystemer sker strengt efter prioritetsniveau. Når den aktuelle tråd giver op kontrol, får den ventende tråd med højeste prioritet kontrol. (En undtagelse fra denne regel er Windows 3.x, der bruger en samarbejdsmodel, men ikke har meget af en planlægning. Det vindue, der har fokus, får kontrol.)