Java samtidighet

Programmeringsspråket Java och den virtuella Java-maskinen (JVM) har utformats för att stödja samtidig programmering , och all körning sker i kontexten av trådar . Objekt och resurser kan nås av många separata trådar; varje tråd har sin egen körningsväg men kan potentiellt komma åt vilket objekt som helst i programmet. Programmeraren måste säkerställa att läs- och skrivåtkomst till objekt är korrekt koordinerad (eller " synkroniserad ") mellan trådar. Trådsynkronisering säkerställer att objekt endast modifieras av en tråd åt gången och att trådar förhindras från att komma åt delvis uppdaterade objekt under modifiering av en annan tråd. Java-språket har inbyggda konstruktioner för att stödja denna koordinering.

Processer och trådar

De flesta implementeringar av den virtuella Java-maskinen körs som en enda process och i programmeringsspråket Java handlar samtidig programmering mest om trådar (även kallade lättviktsprocesser ). Flera processer kan endast realiseras med flera JVM.

Trä föremål

Trådar delar processens resurser, inklusive minne och öppna filer. Detta ger effektiv men potentiellt problematisk kommunikation. Varje applikation har minst en tråd som kallas huvudtråden. Huvudtråden har möjlighet att skapa ytterligare trådar som körbara eller anropbara objekt. (Det Callable- gränssnittet liknar Runnable , genom att båda är designade för klasser vars instanser potentiellt exekveras av en annan tråd. En Runnable returnerar dock inte ett resultat och kan inte skicka ett markerat undantag.)

Varje tråd kan schemaläggas på en annan CPU-kärna eller använda tidsdelning på en enda hårdvaruprocessor, eller tidsdelning på många hårdvaruprocessorer. Det finns ingen generell lösning på hur Java-trådar mappas till inbyggda OS-trådar. Varje JVM-implementering kan göra det på ett annat sätt.

Varje tråd är associerad med en instans av klassen Thread. Trådar kan hanteras antingen direkt med Thread-objekt eller med abstrakta mekanismer som Executor s och java.util.concurrent collections.

Startar en tråd

Två sätt att starta en tråd:

Ge ett körbart objekt
      
    
       
        
    
         
          
    
  public  class  HelloRunnable  implementerar  Runnable  {  @Override  public  void  run  ()  {  System  .  ut  .  println  (  "Hej från tråden!")  ;  }  public  static  void  main  (  String  []  args  )  {  (  ny  tråd  (  new  HelloRunnable  ())).  start  ();  }  } 
Underklass tråd
      
    
       
        
    
         
         
    
  public  class  HelloThread  utökar  tråden  {  @Override  public  void  run  ()  {  System  .  ut  .  println  (  "Hej från tråden!")  ;  }  public  static  void  main  (  String  []  args  )  {  (  new  HelloThread  ()).  start  ();  }  } 

Avbryter

Ett avbrott är en indikation till en tråd att den ska sluta med vad den gör och göra något annat. En tråd skickar ett avbrott genom att anropa avbrott på trådobjektet för tråden som ska avbrytas. Avbrottsmekanismen implementeras med hjälp av en intern flagga känd som avbrottsstatus. Att anropa Thread.interrupt sätter denna flagga. Enligt konvention rensar varje metod som avslutas genom att kasta en InterruptedException avbrottsstatus när den gör det. Det är dock alltid möjligt att avbrottsstatus omedelbart ställs in igen, genom att en annan tråd anropar avbrott.

Går med

Thread.join - metoderna tillåter en tråd att vänta på att en annan är klar.

Undantag

Oupptäckta undantag från kod kommer att avsluta tråden. Huvudtråden skriver ut undantag till konsolen, men användarskapade trådar behöver en hanterare registrerad för att göra det .

Minnesmodell

Java -minnesmodellen beskriver hur trådar i programmeringsspråket Java interagerar genom minnet. På moderna plattformar exekveras koden ofta inte i den ordning den skrevs. Det ordnas om av kompilatorn , processorn och minnesundersystemet för att uppnå maximal prestanda . Java-programmeringsspråket garanterar inte linjärisering eller ens sekventiell konsistens vid läsning eller skrivning av fält för delade objekt, och detta är för att möjliggöra kompilatoroptimeringar (såsom registerallokering , eliminering av vanliga underuttryck och eliminering av redundant läs ) som alla fungerar genom att ändra ordning på minnesläsningar—skriver.

Synkronisering

Trådar kommunicerar främst genom att dela åtkomst till fält och de objekt som referensfält refererar till. Denna form av kommunikation är extremt effektiv, men gör två typer av fel möjliga: trådstörningar och minneskonsistensfel. Det verktyg som behövs för att förhindra dessa fel är synkronisering.

Omordningar kan spela in i felaktigt synkroniserade flertrådade program, där en tråd kan observera effekterna av andra trådar, och kanske kan upptäcka att variabla åtkomster blir synliga för andra trådar i en annan ordning än som exekveras eller specificeras i programmet. För det mesta bryr sig den ena tråden inte vad den andra gör. Men när den gör det, är det vad synkronisering är till för.

För att synkronisera trådar använder Java monitorer , som är en högnivåmekanism för att endast tillåta en tråd åt gången att exekvera en kodregion som skyddas av monitorn. Monitorernas beteende förklaras i termer av lås ; det finns ett lås kopplat till varje objekt.

Synkronisering har flera aspekter. Det mest välkända är ömsesidig uteslutning — endast en tråd kan hålla en bildskärm samtidigt, så synkronisering på en bildskärm innebär att när en tråd går in i ett synkroniserat block som skyddas av en bildskärm, kan ingen annan tråd komma in i ett block som skyddas av den bildskärmen förrän den första tråden lämnar det synkroniserade blocket.

Men det finns mer med synkronisering än ömsesidig uteslutning. Synkronisering säkerställer att minnesskrivningar av en tråd före eller under ett synkroniserat block görs synliga på ett förutsägbart sätt för andra trådar som synkroniserar på samma monitor. Efter att vi avslutat ett synkroniserat block släpper vi monitorn, vilket har effekten att tömma cachen till huvudminnet, så att skrivningar gjorda av denna tråd kan vara synliga för andra trådar. Innan vi kan gå in i ett synkroniserat block skaffar vi monitorn, vilket har effekten att ogiltigförklara den lokala processorcachen så att variabler laddas om från huvudminnet. Vi kommer då att kunna se alla skrivningar som gjorts synliga av den tidigare utgåvan.

Läsningar – skrivningar till fält är linjäriserbara om antingen fältet är flyktigt eller om fältet skyddas av ett unikt lås som förvärvas av alla läsare och skribenter.

Lås och synkroniserade block

En tråd kan uppnå ömsesidig uteslutning antingen genom att ange ett synkroniserat block eller en metod, som skaffar ett implicit lås, eller genom att förvärva ett explicit lås (som ReentrantLock från paketet java.util.concurrent.locks). Båda tillvägagångssätten har samma implikationer för minnesbeteende. Om alla åtkomster till ett visst fält är skyddade av samma lås, då läser - skrivningar till det fältet är linjäriserbara (atomära).

Flyktiga fält

garanterar Java volatile att:

  1. (I alla versioner av Java) Det finns en global ordning på läsning och skrivning till en volatil variabel. Detta innebär att varje tråd som kommer åt ett flyktigt fält kommer att läsa dess nuvarande värde innan du fortsätter, istället för att (potentiellt) använda ett cachat värde. (Det finns dock ingen garanti om den relativa ordningen av flyktiga läsningar och skrivningar med vanliga läsningar och skrivningar, vilket innebär att det i allmänhet inte är en användbar trådkonstruktion.)
  2. (I Java 5 eller senare) Volatile läsningar och skrivningar etablerar en händer-före-relation , ungefär som att skaffa och släppa en mutex. Detta förhållande är helt enkelt en garanti för att minnesskrivningar av ett specifikt påstående är synliga för ett annat specifikt påstående.

Flyktiga fält är linjäriserbara. Att läsa ett flyktigt fält är som att skaffa ett lås: arbetsminnet ogiltigförklaras och det flyktiga fältets aktuella värde läses om från minnet. Att skriva ett flyktigt fält är som att släppa ett lås: det flyktiga fältet skrivs omedelbart tillbaka till minnet.

Sista fälten

Ett fält som förklarats vara slutgiltigt kan inte ändras när det väl har initierats. Ett objekts slutliga fält initieras i dess konstruktor. Om konstruktören följer vissa enkla regler, kommer det korrekta värdet för eventuella slutliga fält att vara synligt för andra trådar utan synkronisering. Regeln är enkel: denna referens får inte släppas från konstruktorn innan konstruktorn återvänder.

Historia

Sedan JDK 1.2 har Java inkluderat en standarduppsättning samlingsklasser, Java collections framework

Doug Lea , som också deltog i implementeringen av Java-samlingsramverket, utvecklade ett samtidighetspaket, som omfattar flera samtidighetsprimitiver och ett stort batteri av samlingsrelaterade klasser. Detta arbete fortsatte och uppdaterades som en del av JSR 166 som leddes av Doug Lea.

JDK 5.0 inkorporerade många tillägg och förtydliganden till Java samtidighetsmodellen. Samtidighets-API:erna som utvecklats av JSR 166 inkluderades också som en del av JDK för första gången. JSR 133 gav stöd för väldefinierade atomoperationer i en flertrådad/multiprocessormiljö.

Både Java SE 6 och Java SE 7 lanserade uppdaterade versioner av JSR 166 API:er samt flera nya ytterligare API:er.

Se även

Anteckningar

externa länkar