Java >> Java tutorial >  >> Tag >> public

Kan en tråd først erhverve et objekt via sikker publicering og derefter publicere det på en usikker måde?

Delvist svar:hvordan "usikker genudgivelse" fungerer på OpenJDK i dag.
(Dette er ikke det ultimative generelle svar, jeg gerne vil have, men det viser i det mindste, hvad man kan forvente på den mest populære Java-implementering)

Kort sagt afhænger det af, hvordan objektet blev offentliggjort i starten:

  1. hvis den første udgivelse sker gennem en flygtig variabel, så er "usikker genudgivelse" sandsynligvis sikkert, dvs. du vil sandsynligvis se aldrig objektet som delvist konstrueret
  2. hvis den første udgivelse sker gennem en synkroniseret blok, så er "usikker genudgivelse" sandsynligvis usikre, dvs. du vil sandsynligvis kunne se objektet som delvist konstrueret

Sandsynligvis er fordi jeg baserer mit svar på assembly genereret af JIT til mit testprogram, og da jeg ikke er ekspert i JIT, ville det ikke overraske mig, hvis JIT genererede en helt anden maskinkode på en andens computer.

Til test brugte jeg OpenJDK 64-Bit Server VM (build 11.0.9+11-alpine-r1, blandet tilstand) på ARMv8.
ARMv8 blev valgt, fordi den har en meget afslappet hukommelsesmodel, som kræver hukommelsesbarriere-instruktioner i både udgiver- og læser-tråde (i modsætning til x86).

1. Indledende offentliggørelse gennem en flygtig variabel:sandsynligvis sikker

Test java-programmet er som i spørgsmålet (jeg tilføjede kun en tråd mere for at se, hvilken samlingskode der genereres for en flygtig skrivning):

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(value = 1,
    jvmArgsAppend = {"-Xmx512m", "-server", "-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintAssembly",
        "-XX:+PrintInterpreter", "-XX:+PrintNMethods", "-XX:+PrintNativeNMethods",
        "-XX:+PrintSignatureHandlers", "-XX:+PrintAdapterHandlers", "-XX:+PrintStubCode",
        "-XX:+PrintCompilation", "-XX:+PrintInlining", "-XX:+TraceClassLoading",})
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(4)
public class VolTest {

  static class Obj1 {
    int f1 = 0;
  }

  @State(Scope.Group)
  public static class State1 {
    volatile Obj1 v1 = new Obj1();
    Obj1 v2 = new Obj1();
  }

  @Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
  public void runVolT1(State1 s) {
    Obj1 o = new Obj1();  /* 43 */
    o.f1 = 1;             /* 44 */
    s.v1 = o;             /* 45 */
  }

  @Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
  public void runVolT2(State1 s) {
    s.v2 = s.v1;          /* 52 */
  }

  @Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
  public int runVolT3(State1 s) {
    return s.v1.f1;       /* 59 */
  }

  @Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
  public int runVolT4(State1 s) {
    return s.v2.f1;       /* 66 */
  }
}

Her er samlingen genereret af JIT til runVolT3 og runVolT4 :

Compiled method (c1)   26806  529       2       org.sample.VolTest::runVolT3 (8 bytes)
  ...
[Constants]
  # {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
  # this:     c_rarg1:c_rarg1
                        = 'org/sample/VolTest'
  # parm0:    c_rarg2:c_rarg2
                        = 'org/sample/VolTest$State1'
  ...
[Verified Entry Point]
  ...
                                                ;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 59)

  0x0000fff781a60938: dmb       ish
  0x0000fff781a6093c: ldr       w0, [x2, #12]   ; implicit exception: dispatches to 0x0000fff781a60984
  0x0000fff781a60940: dmb       ishld           ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 59)

  0x0000fff781a60944: ldr       w0, [x0, #12]   ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 59)
                                                ; implicit exception: dispatches to 0x0000fff781a60990
  0x0000fff781a60948: ldp       x29, x30, [sp, #48]
  0x0000fff781a6094c: add       sp, sp, #0x40
  0x0000fff781a60950: ldr       x8, [x28, #264]
  0x0000fff781a60954: ldr       wzr, [x8]       ;   {poll_return}
  0x0000fff781a60958: ret

...

Compiled method (c2)   27005  536       4       org.sample.VolTest::runVolT3 (8 bytes)
  ...
[Constants]
  # {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
  # this:     c_rarg1:c_rarg1
                        = 'org/sample/VolTest'
  # parm0:    c_rarg2:c_rarg2
                        = 'org/sample/VolTest$State1'
  ...
[Verified Entry Point]
  ...
                                                ; - org.sample.VolTest::[email protected] (line 59)
  0x0000fff788f692f4: cbz       x2, 0x0000fff788f69318
  0x0000fff788f692f8: add       x10, x2, #0xc
  0x0000fff788f692fc: ldar      w11, [x10]      ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 59)

  0x0000fff788f69300: ldr       w0, [x11, #12]  ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 59)
                                                ; implicit exception: dispatches to 0x0000fff788f69320
  0x0000fff788f69304: ldp       x29, x30, [sp, #16]
  0x0000fff788f69308: add       sp, sp, #0x20
  0x0000fff788f6930c: ldr       x8, [x28, #264]
  0x0000fff788f69310: ldr       wzr, [x8]       ;   {poll_return}
  0x0000fff788f69314: ret

...

Compiled method (c1)   26670  527       2       org.sample.VolTest::runVolT4 (8 bytes)
 ...
[Constants]
  # {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
  # this:     c_rarg1:c_rarg1 
                        = 'org/sample/VolTest'
  # parm0:    c_rarg2:c_rarg2 
                        = 'org/sample/VolTest$State1'
  ...
[Verified Entry Point]
  ...
                                                ;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 66)

  0x0000fff781a604b8: ldr       w0, [x2, #16]   ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 66)
                                                ; implicit exception: dispatches to 0x0000fff781a604fc
  0x0000fff781a604bc: ldr       w0, [x0, #12]   ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 66)
                                                ; implicit exception: dispatches to 0x0000fff781a60508
  0x0000fff781a604c0: ldp       x29, x30, [sp, #48]
  0x0000fff781a604c4: add       sp, sp, #0x40
  0x0000fff781a604c8: ldr       x8, [x28, #264]
  0x0000fff781a604cc: ldr       wzr, [x8]       ;   {poll_return}
  0x0000fff781a604d0: ret

...

Compiled method (c2)   27497  535       4       org.sample.VolTest::runVolT4 (8 bytes)
  ...
[Constants]
  # {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
  # this:     c_rarg1:c_rarg1
                        = 'org/sample/VolTest'
  # parm0:    c_rarg2:c_rarg2
                        = 'org/sample/VolTest$State1'
  ...
[Verified Entry Point]
  ...
                                                ; - org.sample.VolTest::[email protected] (line 66)
  0x0000fff788f69674: ldr       w11, [x2, #16]  ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 66)
                                                ; implicit exception: dispatches to 0x0000fff788f69690
  0x0000fff788f69678: ldr       w0, [x11, #12]  ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
                                                ; - org.sample.VolTest::[email protected] (line 66)
                                                ; implicit exception: dispatches to 0x0000fff788f69698
  0x0000fff788f6967c: ldp       x29, x30, [sp, #16]
  0x0000fff788f69680: add       sp, sp, #0x20
  0x0000fff788f69684: ldr       x8, [x28, #264]
  0x0000fff788f69688: ldr       wzr, [x8]       ;   {poll_return}
  0x0000fff788f6968c: ret

Lad os bemærke, hvilke barriereinstruktioner den genererede samling indeholder:

  • runVolT1 (samlingen er ikke vist ovenfor, fordi den er for lang):
    • c1 version indeholder 1x dmb ishst , 2x dmb ish
    • c2 version indeholder 1x dmb ishst , 1x dmb ish , 1x stlr
  • runVolT3 (som læser flygtig v1 ):
    • c1 version 1x dmb ish , 1x dmb ishld
    • c2 version 1x ldar
  • runVolT4 (som læser ikke-flygtige v2 ):ingen hukommelsesbarrierer

Som du kan se, runVolT4 (som læser objektet efter usikker genudgivelse) indeholder ikke hukommelsesbarrierer.

Betyder det, at tråden kan se objekttilstanden som semi-initialiseret?
Viser sig nej, på ARMv8 er det alligevel sikkert.

Hvorfor?
Se på return s.v2.f1; i koden. Her udfører CPU 2 hukommelseslæsninger:

  • først læser den s.v2 , som indeholder hukommelsesadressen for objekt o
  • så læser den værdien o.f1 fra (hukommelsesadresse på o ) + (forskydning af felt f1 inden for Obj1 )

Hukommelsesadressen for o.f1 read beregnes ud fra værdien returneret af s.v2 læs — dette er såkaldt "adresseafhængighed".

På ARMv8 forhindrer en sådan adresseafhængighed genbestilling af disse to læsninger (se MP+dmb.sy+addr eksempel i modellering af ARMv8-arkitekturen, operationelt:concurrency og ISA, du kan prøve det selv i ARM's Memory Model Tool) — så vi er garanteret at se v2 som fuldt initialiseret.

Hukommelsesbarriere-instruktioner i runVolT3 tjener forskellige formål:de forhindrer genbestilling af den flygtige læsning af s.v1 med andre handlinger i tråden (i Java er en flygtig læsning en af ​​synkroniseringshandlinger, som skal være helt i orden).

Mere end det, viser det sig i dag på alle de understøttede af OpenJDK-arkitekturer, adresseafhængighed forhindrer genbestilling af læsninger (se "Afhængige belastninger kan omarrangeres" i denne tabel i wiki eller "Dataafhængighed bestiller belastninger?" i tabellen i JSR-133 Kogebog for kompilatorforfattere).

Som et resultat heraf, i dag på OpenJDK, hvis et objekt oprindeligt publiceres gennem et flygtigt felt, så vil det højst sandsynligt være synligt som fuldt initialiseret selv efter usikker genpublicering.

2. Indledende udgivelse gennem en synkroniseret blok:højst sandsynligt usikker

Situationen er anderledes, når den første udgivelse sker gennem en synkroniseret blok:

class Obj1 {
  int f1 = 0;
}

Obj1 v1;
Obj1 v2;

Thread 1              | Thread 2       | Thread 3
--------------------------------------------------------
synchronized {        |                |
  var o = new Obj1(); |                |
  o.f1 = 1;           |                |
  v1 = o;             |                |
}                     |                |
                      | synchronized { |
                      |   var r1 = v1; |
                      | }              |
                      | v2 = r1;       |
                      |                | var r2 = v2.f1;

Is (r2 == 0) possible?

Her den genererede assembly for Thread 3 er det samme som for runVolT4 ovenfor:den indeholder ingen hukommelsesbarriere-instruktioner. Som følge heraf Thread 3 kan nemt se skrivninger fra Thread 1 ude af drift.

Og generelt er usikker genudgivelse i sådanne tilfælde højst sandsynligt usikker i dag på OpenJDK.


Svar:Kausalitetsdelen af ​​JMM tillader Thread 3 for at se o som delvist konstrueret.

Det lykkedes mig endelig at anvende 17.4.8. Udførelser og kausalitetskrav (aka kausalitetsdelen af ​​JMM) til dette eksempel.

Så dette er vores Java-program:

class Obj1 {
  int f1;
}

volatile Obj1 v1;
Obj1 v2;

Thread 1            | Thread 2 | Thread 3
--------------------|----------|-----------------
var o = new Obj1(); |          |
o.f1 = 1;           |          |
v1 = o;             |          |
                    | v2 = v1; |
                    |          | var r1 = v2.f1;

Og vi ønsker at finde ud af, om resultatet (r1 == 0) er tilladt.

Det viser sig, for at bevise, at (r1 == 0) er tilladt, skal vi finde en velformet udførelse , som giver det resultat og kan valideres med algoritmen givet i 17.4.8. Udførelser og kausalitetskrav.

Lad os først omskrive vores Java-program med hensyn til variabler og handlinger som defineret i algoritmen.
Lad os også vise værdierne for vores læse- og skrivehandlinger for at få udførelsen E vi ønsker at validere:

Initially: W[v1]=null, W[v2]=null, W[o.f1]=0

Thread 1  | Thread 2 | Thread 3
----------|----------|-----------
W[o.f1]=1 |          |
Wv[v1]=o  |          |
          | Rv[v1]=o |
          | W[v2]=o  |
          |          | R[v2]=o
          |          | R[o.f1]=0

Bemærkninger:

  • o repræsenterer den instans, der er oprettet af new Obj1(); i java-koden
  • W og R repræsentere normal skrivning og læsning; Wv og Rv repræsenterer flygtige skrivninger og læsninger
  • læst/skrevet værdi for handlingen vises efter =
  • W[o.f1]=0 er i de indledende handlinger, fordi ifølge JLS:

    Skrivningen af ​​standardværdien (nul, falsk eller null) til hver variabel synkroniseres - med den første handling i hver tråd.
    Selvom det kan virke lidt mærkeligt at skrive en standardværdi til en variabel, før objektet, der indeholder variablen, er allokeret, så oprettes konceptuelt hvert objekt i starten af ​​programmet med dets initialiserede standardværdier.

Her er en mere kompakt form for E :

W[v1]=null, W[v2]=null, W[o.f1]=0
---------------------------------
W[o.f1]=1 |          |
Wv[v1]=o  |          |
          | Rv[v1]=o |
          | W[v2]=o  |
          |          | R[v2]=o
          |          | R[o.f1]=0

Validering af E

Ifølge 17.4.8. Udførelser og kausalitetskrav:

En velformet udførelse E = valideres ved at begå handlinger fra A. Hvis alle handlingerne i A kan begås, så opfylder udførelsen kausalitetskravene af hukommelsesmodellen for Java-programmeringssproget.

Så vi er nødt til at bygge trin-for-trin sættet af engagerede handlinger (vi får en sekvens C₀,C₁,... , hvor Cₖ er sættet af forpligtede handlinger på den k-te iteration og Cₖ ⊆ Cₖ₊₁ ), indtil vi udfører alle handlinger A af vores udførelse E .
JLS sektionen indeholder også 9 regler, som definerer, hvornår en handling kan begås.

  • Trin 0:Algoritmen starter altid med et tomt sæt.

    C₀ = ∅
    
  • Trin 1:vi forpligter kun skriverier.
    Årsagen er, at i henhold til regel 7, har en begået en læsning i Сₖ skal returnere en skrivning fra Сₖ₋₁ , men vi har tom C₀ .

    E₁:
    
    W[v1]=null, W[v2]=null, W[o.f1]=0
    ----------------------------------
    W[o.f1]=1 |          |
    Wv[v1]=o  |          |
    
    C₁ = { W[v1]=null, W[v2]=null, W[o.f1]=0, W[o.f1]=1, Wv[v1]=o }
    
  • Trin 2:nu kan vi læse og skrive o i tråd 2.
    Siden v1 er flygtig, Wv[v1]=o sker-før Rv[v1] , og læsningen returnerer o .

    E₂:
    
    W[v1]=null, W[v2]=null, W[o.f1]=0
    ---------------------------------
    W[o.f1]=1 |          |
    Wv[v1]=o  |          |
              | Rv[v1]=o |
              | W[v2]=o  |
    
    C₂ = C₁∪{ Rv[v1]=o, W[v2]=o }
    
  • Trin 3:nu har vi W[v2]=o begået, kan vi begå den læste R[v2] i tråd 3.
    Ifølge regel 6 kan en aktuelt begået læsning kun returnere en sker-før-skrivning (værdien kan ændres én gang til en hurtig skrivning på næste trin).
    R[v2] og W[v2]=o er ikke bestilt med happens-before, så R[v2] læser null .

    E₃:
    
    W[v1]=null, W[v2]=null, W[o.f1]=0
    ---------------------------------
    W[o.f1]=1 |          |
    Wv[v1]=o  |          |
              | Rv[v1]=o |
              | W[v2]=o  |
              |          | R[v2]=null
    
    C₃ = C₂∪{ R[v2]=null }
    
  • Trin 4:nu R[v2] kan læse W[v2]=o gennem et dataræs, og det gør R[o.f1] muligt.
    R[o.f1] læser standardværdien 0 , og algoritmen afsluttes, fordi alle handlingerne i vores udførelse er begået.

    E = E₄:
    
    W[v1]=null, W[v2]=null, W[o.f1]=0
    ---------------------------------
    W[o.f1]=1 |          |
    Wv[v1]=o  |          |
              | Rv[v1]=o |
              | W[v2]=o  |
              |          | R[v2]=o
              |          | R[o.f1]=0
    
    A = C₄ = C₂∪{ R[v2]=o, R[o.f1]=0 }
    

Som et resultat validerede vi en udførelse, som producerer (r1 == 0) , derfor er dette resultat gyldigt.

Det er også værd at bemærke, at denne kausalitetsvalideringsalgoritme tilføjer næsten ingen yderligere begrænsninger til happens-before.
Jeremy Manson (en af ​​JMM-forfatterne) forklarer, at algoritmen eksisterer for at forhindre en ret bizar adfærd - såkaldte "kausalitetsløkker", når der er en cirkulær kæde af handlinger, som forårsager hinanden (dvs. når en handling forårsager sig selv).
I alle andre tilfælde, bortset fra disse kausalitetsløkker, bruger vi happens-before som i Toms kommentar.


Java tag