Java >> Java tutorial >  >> Tag >> final

Java endelige felter:er tyveri-adfærd mulig med den aktuelle JLS

Ja , det er tilladt.

Hovedsageligt eksponeret på de allerede citerede afsnit af JMM :

Under forudsætning af, at objektet er konstrueret "korrekt", når et objekt er konstrueret, vil de værdier, der er tildelt de sidste felter i konstruktøren, være synlige for alle andre tråde uden synkronisering .

Hvad betyder det, at et objekt er korrekt konstrueret ? Det betyder ganske enkelt, at ingen henvisning til det objekt, der konstrueres, må "undslippe" under konstruktionen .

Med andre ord, placer ikke en reference til det objekt, der bliver konstrueret hvor som helst, hvor en anden tråd kan se det; tildel det ikke til et statisk felt, registrer det ikke som alistener med noget andet objekt, og så videre. Disse opgaver skal udføres, efter at konstruktøren er færdig, ikke i konstruktøren ***

Så ja, det er muligt, så vidt det er tilladt. Sidste afsnit er fyldt med forslag til hvordan man ikke gør ting; Når nogen siger, undgå at gøre X , så er implicit, at X kan gøres.

Hvad nu hvis... reflection

De andre svar påpeger korrekt kravene til, at de endelige felter skal ses korrekt af andre tråde, såsom fastfrysningen i enden af ​​konstruktøren, kæden og så videre. Disse svar giver en dybere forståelse af hovedspørgsmålet og bør læses først. Denne fokuserer på en mulig undtagelse fra disse regler.

Den mest gentagne regel/sætning kan være denne her, kopieret fra Eugene 's svar (som ikke burde have nogen negativ stemme btw ):

Et objekt anses for at være fuldstændig initialiseret, når dets konstruktør er færdig. En tråd, der kun kan se en reference til et objekt, efter at objektet er blevet fuldstændig initialiseret, er garanteret at se de korrekte [tildelte/indlæste/indstillede] værdier for objektets sidste felter .

Bemærk, at jeg har ændret udtrykket "initialiseret" med de tilsvarende termer tildelt, indlæst eller indstillet. Dette er med hensigten, da terminologien kan vildlede min pointe her.

En anden korrekt udtalelse er den fra chrylis -cautiouslyoptimistic- :

Den "endelige fastfrysning" sker i slutningen af ​​konstruktøren, og fra det tidspunkt er alle læsninger garanteret nøjagtige.

JLS 17.5 endelig feltsemantik anføre, at:

En tråd, der kun kan se en reference til et objekt, efter at objektet er blevet fuldstændig initialiseret, vil garanteret se de korrekt initialiserede værdier for objektets sidste felter .

Men, tror du, at refleksion giver et fjols omkring dette? Nej selvfølgelig ikke. Den læste ikke engang det afsnit.

Efterfølgende ændring af final Felter

Disse udsagn er ikke kun korrekte, men understøttes også af JLS . Jeg har ikke til hensigt at tilbagevise dem, men tilføjer bare lidt ekstra information om en undtagelse fra denne lov:refleksion . Den mekanisme, der blandt andet kan ændre et endeligt felts værdi efter initialisering .

Fastfrysning af en final feltet opstår i slutningen af ​​konstruktøren, hvor final felt er sat, det er helt rigtigt. Men der er en anden trigger for frysningsoperationen, som ikke er blevet taget i betragtning:Frys af en final felt forekommer også initialisering/ændring af et felt via refleksion (JLS 17.5.3):

Frysninger af et endeligt felt forekommer både i slutningen af ​​konstruktøren, hvori det endelige felt er sat, og umiddelbart efter hver ændring af et endeligt felt via refleksion .

Reflekterende operationer på final felter "bryder" reglen:efter at konstruktøren er blevet korrekt færdig, læser alle final felter er stadig IKKE garanteret for at være nøjagtige . Jeg ville prøve at forklare.

Lad os forestille os, at alt det rigtige flow er blevet respekteret, konstruktøren er blevet initialiseret og alle final felter fra en instans ses korrekt af en tråd. Nu er det tid til at foretage nogle ændringer på disse felter via refleksion (forestil dig, at det er nødvendigt, selvom det er usædvanligt, jeg ved det.. ).

De tidligere regler følges, og alle tråde venter, indtil alle felter er blevet opdateret:ligesom med det sædvanlige konstruktørscenarie, tilgås felterne først, efter at de er blevet fryse, og den reflekterende operation er blevet korrekt afsluttet. Det er her, loven er brudt :

Hvis et endeligt felt initialiseres til et konstant udtryk (§15.28) i felterklæringen, vil ændringer i det endelige felt muligvis ikke blive observeret, da brug af det sidste felt erstattes på kompileringstidspunktet med værdien af ​​det konstante udtryk.

Dette er sigende:Selv hvis alle regler blev fulgt, vil din kode ikke læse final korrekt. feltets tildelte værdi, hvis denne variabel er en primitiv eller streng og du initialiserede det som et konstant udtryk i felterklæringen . Hvorfor? Fordi den variabel kun er en hardkodet værdi for din compiler, som aldrig vil tjekke feltet igen eller dets ændringer, selvom din kode korrekt opdaterede værdien i runtime-udførelsen.

Så lad os teste det:

 public class FinalGuarantee 
 {          
      private final int  i = 5;  //initialized as constant expression
      private final long l;

      public FinalGuarantee() 
      {
         l = 1L;
      }
        
      public static void touch(FinalGuarantee f) throws Exception
      {
         Class<FinalGuarantee> rfkClass = FinalGuarantee.class;
         Field field = rfkClass.getDeclaredField("i");
         field.setAccessible(true);
         field.set(f,555);                      //set i to 555
         field = rfkClass.getDeclaredField("l");
         field.setAccessible(true);
         field.set(f,111L);                     //set l to 111                 
      }
      
      public static void main(String[] args) throws Exception 
      {
         FinalGuarantee f = new FinalGuarantee();
         System.out.println(f.i);
         System.out.println(f.l);
         touch(f);
         System.out.println("-");
         System.out.println(f.i);
         System.out.println(f.l);
      }    
 }

Output :

 5
 1
 -
 5   
 111

Den endelige int i blev opdateret korrekt under kørsel, og for at kontrollere det kunne du fejlsøge og inspicere objektets feltværdier:

Begge i og l var korrekt opdateret. Så hvad sker der med i , hvorfor viser 5 stadig? Fordi som angivet på JLS , feltet i erstattes direkte på kompileringstidspunktet med værdien af ​​det konstante udtryk , som i dette tilfælde er 5 .

Hver efterfølgende læsning af det sidste felt i vil så være FORKERT , selvom alle tidligere regler blev fulgt. Compileren vil aldrig igen tjekke dette felt:Når du koder f.i , vil den ikke få adgang til nogen variabel af nogen instans. Det vil bare returnere 5:det sidste felt er bare hårdkodet på kompileringstidspunktet og hvis der laves en opdatering på den på runtime, vil den aldrig, aldrig blive set korrekt igen af ​​nogen tråd. Dette bryder loven .

Som bevis på den korrekte opdatering af felterne ved kørsel:

Begge 555 og 111L skubbes ind i stakken, og felterne får deres nytildelte værdier. Men hvad sker der, når man manipulerer dem, såsom at udskrive deres værdi?

  • l blev ikke initialiseret til et konstant udtryk eller i feltdeklarationen. Som følge heraf påvirkes den ikke af 17.5.3 's regel. Feltet er korrekt opdateret og læst fra ydre tråde.

  • i , blev imidlertid initialiseret til et konstant udtryk i feltdeklarationen. Efter den første fastfrysning er der ikke mere f.i for compileren vil det felt aldrig blive tilgået igen. Også selvom variablen er korrekt opdateret til 555 i eksemplet er hvert forsøg på at læse fra feltet blevet erstattet af den hårdkodede konstant 5; uanset enhver yderligere ændring/opdatering foretaget på variablen, vil den altid returnere fem.

16: before the update
42: after the update

Ingen feltadgang, men bare et "ja, det er helt sikkert 5, returner det ". Dette indebærer, at en final felt er ikke ALTID garanteret at blive set korrekt fra ydre tråde, selvom alle protokoller blev fulgt.

Dette påvirker primitiver og strenge. Jeg ved godt, at det er et usædvanligt scenarie, men det er stadig muligt.

Nogle andre problematiske scenarier (nogle var også relateret til synkroniseringsproblemet citeret i kommentarerne ):

1- Hvis det ikke er korrekt synchronized med den reflekterende operation kunne en tråd falde i en løbstilstand i følgende scenarie:

    final boolean flag;  // false in constructor
    final int x;         // 1 in constructor 
  • Lad os antage, at reflektionsoperationen vil, i denne rækkefølge:
  1- Set flag to true
  2- Set x to 100.

Forenkling af læsetrådens kode:

    while (!instance.flag)  //flag changes to true
       Thread.sleep(1);
    System.out.println(instance.x); // 1 or 100 ?

Som et muligt scenario havde den reflekterende handling ikke tid nok til at opdatere x , så final int x feltet kan eller ikke læses korrekt.

2- En tråd kan falde i en deadlock i følgende scenarie:

    final boolean flag;  // false in constructor
  • Lad os antage, at reflektionsoperationen vil:
  1- Set flag to true

Forenkling af læsetrådens kode:

    while (!instance.flag) { /*deadlocked here*/ } 

    /*flag changes to true, but the thread started to check too early.
     Compiler optimization could assume flag won't ever change
     so this thread won't ever see the updated value. */

Jeg ved, at dette ikke er et specifikt problem for endelige felter, men blot tilføjet som et muligt scenarie med ukorrekt læseflow af denne type variabler. Disse to sidste scenarier ville blot være en konsekvens af forkerte implementeringer, men ville gerne påpege dem.


Ja, sådan adfærd er tilladt.

Det viser sig, at en detaljeret forklaring af samme sag er tilgængelig på den personlige side af William Pugh (endnu en JMM-forfatter):Ny præsentation/beskrivelse af semantikken i endelige felter.

Kort version:

  • afsnit 17.5.1. Semantik af endelige felter i JLS definerer særlige regler for endelige felter.
    Reglerne lader os grundlæggende etablere en yderligere happens-before-relation mellem initialiseringen af ​​et endeligt felt i en konstruktør og en læsning af feltet i en anden tråd, selvom objektet er publiceret via et dataløb.
    Denne yderligere sker-før-relation kræver, at hver stien fra feltinitialiseringen til dens læsning i en anden tråd inkluderede en speciel kæde af handlinger:

    w  ʰᵇ ► f  ʰᵇ ► a  ᵐᶜ ► r1  ᵈᶜ ► r2, where:
    • w er en skrivning til det sidste felt i en konstruktør
    • f er "fryse handling", som sker, når konstruktør afslutter
    • a er en udgivelse af objektet (f.eks. lagring af det til en delt variabel)
    • r₁ er en læsning af objektets adresse i en anden tråd
    • r₂ er en læsning af det sidste felt i samme tråd som r₁ .
  • koden i spørgsmålet har en sti fra o.f1 = 42 til k = r2.f1; som ikke inkluderer den påkrævede freeze o.f handling:

    o.f1 = 42  ʰᵇ ► { freeze o.f is missing }  ʰᵇ ► p = o  ᵐᶜ ► r1 = p  ᵈᶜ ► k = r2.f1

    Som et resultat, o.f1 = 42 og k = r2.f1 er ikke bestilt med happens-before ⇒ vi har et dataræs og k = r2.f1 kan læse 0 eller 42.

Et citat fra Ny præsentation/beskrivelse af de endelige felters semantik:

For at afgøre, om en læsning af et endeligt felt er garanteret at se den initialiserede værdi af det pågældende felt, skal du fastslå, at der ikke er nogen måde at konstruere de delordrer ᵐᶜ ► og ᵈᶜ. ► uden at angive kæden w ʰᵇ. f ʰᵇ. a ᵐᶜ. r₁ ᵈᶜ. r₂ fra skrivning af feltet til læsning af feltet.

...

Skriv i tråd 1 og læs i tråd 2 af p er involveret i en hukommelseskæde. Skriv i tråd 1 og læs i tråd 2 af q er også involveret i en hukommelseskæde. Begge læser f se den samme variabel. Der kan være en dereferencekæde fra læsningerne af f til enten læsningen af ​​p eller læsning af q , fordi de læser den samme adresse. Hvis dereferencekæden er fra læsningen af ​​p , så er der ingen garanti for, at r5 vil se værdien 42.

Bemærk, at for tråd 2 bestiller respektkæden r2 = p ᵈᶜ. r5 = r4.f , men ikke bestil r4 = q ᵈᶜ. r5 = r4.f . Dette afspejler det faktum, at compileren har lov til at flytte enhver læsning af et sidste felt i et objekt o til umiddelbart efter den allerførste læsning af adressen på o i den tråd.


Java tag