Java Language
Atomtyper
Sök…
Introduktion
Java Atomic Types är enkla muterbara typer som tillhandahåller grundläggande operationer som är trådsäkra och atomiska utan att ta till låsning. De är avsedda att användas i fall där låsning skulle vara en samtidigt flaskhals, eller där det finns risk för dödlås eller boskap.
parametrar
Parameter | Beskrivning |
---|---|
uppsättning | Flyktig uppsättning av fältet |
skaffa sig | Flyktig läsning av fältet |
lazySet | Detta är en butik beställd drift av fältet |
compareAndSet | Om värdet är det utskickade värdet skickas det till det nya värdet |
getAndSet | få det aktuella värdet och uppdatera |
Anmärkningar
Många på i huvudsak kombinationer av flyktiga läsningar eller skrivningar och CAS- operationer. Det bästa sättet att förstå detta är att titta på källkoden direkt. Exempelvis AtomicInteger , Unsafe.getAndSet
Skapa atomtyper
För enkel multigängad kod är det acceptabelt att använda synkronisering . Men att använda synkronisering påverkar livligheten och när kodbasen blir mer komplex ökar sannolikheten att du kommer att hamna med Deadlock , Starvation eller Livelock .
I fall av mer komplex samtidighet är Atomic Variables ofta ett bättre alternativ, eftersom det gör det möjligt att få åtkomst till en individuell variabel på ett gängsäkert sätt utan att använda sig av synkroniserade metoder eller kodblock.
Skapa en AtomicInteger
typ:
AtomicInteger aInt = new AtomicInteger() // Create with default value 0
AtomicInteger aInt = new AtomicInteger(1) // Create with initial value 1
På liknande sätt för andra instanstyper.
AtomicIntegerArray aIntArray = new AtomicIntegerArray(10) // Create array of specific length
AtomicIntegerArray aIntArray = new AtomicIntegerArray(new int[] {1, 2, 3}) // Initialize array with another array
På liknande sätt för andra atomtyper.
Det finns ett anmärkningsvärt undantag att det inte finns några float
och double
. Dessa kan simuleras genom användning av Float.floatToIntBits(float)
och Float.intBitsToFloat(int)
för float
samt Double.doubleToLongBits(double)
och Double.longBitsToDouble(long)
för dubbel.
Om du är villig att använda sun.misc.Unsafe
du använda valfri primitiv variabel som atom genom att använda atomoperationen i sun.misc.Unsafe
. Alla primitiva typer bör konverteras eller kodas i int eller länge för att använda det på detta sätt. För mer om detta se: sun.misc.Unsafe .
Motivation för atomtyper
Det enkla sättet att implementera flertrådiga applikationer är att använda Java: s inbyggda synkroniserings- och låsningsprimitiv; t.ex. det synchronized
nyckelordet. Följande exempel visar hur vi kan använda synchronized
att samla räkningar.
public class Counters {
private final int[] counters;
public Counters(int nosCounters) {
counters = new int[nosCounters];
}
/**
* Increments the integer at the given index
*/
public synchronized void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number]++;
}
}
/**
* Obtains the current count of the number at the given index,
* or if there is no number at that index, returns 0.
*/
public synchronized int getCount(int number) {
return (number >= 0 && number < counters.length) ? counters[number] : 0;
}
}
Denna implementering fungerar korrekt. Men om du har ett stort antal trådar som gör massor av samtidiga samtal på samma Counters
objekt kan synkroniseringen vara en flaskhals. Specifikt:
- Varje
synchronized
metodsamtal börjar med att den aktuella tråden förvärvar låset förCounters
instansen. - Tråden kommer att hålla låset medan den kontrollerar
number
värde och uppdaterar räknaren. - Slutligen kommer det att släppa låset, så att andra trådar får åtkomst.
Om en tråd försöker skaffa låset medan en annan håller fast det kommer den försökta tråden att blockeras (stoppas) i steg 1 tills låset lossas. Om flera trådar väntar kommer en av dem att få det, och de andra kommer att fortsätta att blockeras.
Detta kan leda till ett par problem:
Om det finns mycket strid för låset (dvs. massor av tråd försök att skaffa det), kan vissa trådar blockeras under lång tid.
När en tråd är blockerad och väntar på låset försöker operativsystemet vanligtvis byta exekvering till en annan tråd. Denna sammanhangsbytning medför relativt stor prestanda på processorn.
När det finns flera trådar blockerade på samma lås, finns det inga garantier för att någon av dem kommer att behandlas "rättvist" (det vill säga att varje tråd garanteras att planeras att köras). Detta kan leda till gänghunga .
Hur implementerar man Atomtyper?
Låt oss börja med att skriva om exemplet ovan med hjälp av AtomicInteger
räknare:
public class Counters {
private final AtomicInteger[] counters;
public Counters(int nosCounters) {
counters = new AtomicInteger[nosCounters];
for (int i = 0; i < nosCounters; i++) {
counters[i] = new AtomicInteger();
}
}
/**
* Increments the integer at the given index
*/
public void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number].incrementAndGet();
}
}
/**
* Obtains the current count of the object at the given index,
* or if there is no number at that index, returns 0.
*/
public int getCount(int number) {
return (number >= 0 && number < counters.length) ?
counters[number].get() : 0;
}
}
Vi har ersatt int[]
med ett AtomicInteger[]
och initialiserat det med en instans i varje element. Vi har också lagt till samtal till incrementAndGet()
och get()
i stället för operationer på int
värden.
Men det viktigaste är att vi kan ta bort det synchronized
nyckelordet eftersom det inte längre behövs låsning. Detta fungerar eftersom incrementAndGet()
och get()
-operationerna är atomära och gängsäkra . I detta sammanhang betyder det att:
Varje räknare i matrisen kommer endast att observeras i antingen "före" -tillståndet för en operation (som ett "inkrement") eller i "efter" -tillståndet.
Antagande att operationen sker vid tidpunkten
T
, kommer ingen tråd att kunna se "före" -läget efter tidenT
Även om två trådar faktiskt kan försöka uppdatera samma AtomicInteger
instans samtidigt, säkerställer implementeringarna av operationerna att endast ett steg sker i taget på den givna instansen. Detta görs utan låsning, vilket ofta resulterar i bättre prestanda.
Hur fungerar atomtyper?
Atomtyper förlitar sig vanligtvis på specialiserade maskinvaruinstruktioner i instruktionsuppsättningen för målmaskinen. Exempelvis tillhandahåller Intel-baserade instruktionsuppsättningar en CAS
( jämför och byt ) -instruktion som kommer att utföra en specifik sekvens av minnesoperationer atomiskt.
Dessa lågnivåinstruktioner används för att implementera operationer på högre nivå i API: erna för respektive AtomicXxx
klasser. Till exempel (igen, i C-liknande pseudokod):
private volatile num;
int increment() {
while (TRUE) {
int old = num;
int new = old + 1;
if (old == compare_and_swap(&num, old, new)) {
return new;
}
}
}
Om det inte finns någon stridighet på AtomicXxxx
kommer if
testet att lyckas och slingan avslutas omedelbart. Om det finns strid, kommer if
att misslyckas för alla utom en av trådarna, och de "snurrar" i slingan under ett litet antal cykler i slingan. I praktiken är snurrningen större ordningsföljder snabbare (utom vid orealistiskt höga stridighetsnivåer, där synkroniserad fungerar bättre än atomklasser, eftersom när CAS-operationen misslyckas, kommer försöket bara att lägga till mer strid) än att stoppa tråden och byta till en annan ett.
Förresten, CAS-instruktioner används vanligtvis av JVM för att implementera obestämd låsning . Om JVM kan se att ett lås för närvarande inte är låst, kommer det att försöka använda en CAS för att förvärva låset. Om CAS lyckas, finns det inget behov av att göra den dyra trådplaneringen, kontextväxling och så vidare. För mer information om de använda teknikerna, se Partisk låsning i HotSpot .