Zoeken…


Invoering

Java Atomic Types zijn eenvoudige muteerbare typen die basisbewerkingen bieden die draadveilig en atomair zijn zonder toevlucht te nemen tot vergrendeling. Ze zijn bedoeld voor gebruik in gevallen waarin vergrendeling een bottleneck van de gelijktijdigheid zou zijn, of waar er een risico op deadlock of livelock bestaat.

parameters

Parameter Beschrijving
reeks Vluchtige set van het veld
krijgen Vluchtig lezen van het veld
lazySet Dit is een in de winkel geordende bewerking van het veld
compareAndSet Als de waarde de uitgaande waarde is, stuur deze dan naar de nieuwe waarde
getAndSet haal de huidige waarde op en update

Opmerkingen

Veel op in wezen combinaties van vluchtig lezen of schrijven en CAS- bewerkingen. De beste manier om dit te begrijpen, is door direct naar de broncode te kijken. Bijvoorbeeld AtomicInteger , Unsafe.getAndSet

Atoomtypen maken

Voor eenvoudige code met meerdere threads is synchronisatie acceptabel. Het gebruik van synchronisatie heeft echter een impact op de levendigheid en naarmate een codebase complexer wordt, neemt de kans toe dat je met Deadlock , Starvation of Livelock eindigt .

In geval van meer complexe gelijktijdigheid, is het gebruik van Atomic Variables vaak een beter alternatief, omdat hierdoor een individuele variabele op een thread-veilige manier kan worden benaderd zonder de overhead van het gebruik van gesynchroniseerde methoden of codeblokken.

Een AtomicInteger type maken:

AtomicInteger aInt = new AtomicInteger() // Create with default value 0

AtomicInteger aInt = new AtomicInteger(1) // Create with initial value 1

Evenzo voor andere instantietypen.

AtomicIntegerArray aIntArray = new AtomicIntegerArray(10) // Create array of specific length
AtomicIntegerArray aIntArray = new AtomicIntegerArray(new int[] {1, 2, 3}) // Initialize array with another array

Evenzo voor andere atoomtypen.

Er is een opmerkelijke uitzondering dat er geen float en double typen zijn. Deze kunnen worden gesimuleerd door het gebruik van Float.floatToIntBits(float) en Float.intBitsToFloat(int) voor float en Double.doubleToLongBits(double) en Double.longBitsToDouble(long) voor dubbels.

Als u bereid bent sun.misc.Unsafe te gebruiken, kunt u elke primitieve variabele als atomair gebruiken door de atomaire bewerking in sun.misc.Unsafe . Alle primitieve typen moeten worden omgezet of gecodeerd in int of longs om het op deze manier te gebruiken. Zie voor meer informatie: sun.misc.Unsafe .

Motivatie voor atoomtypen

De eenvoudige manier om toepassingen met meerdere threads te implementeren, is door de ingebouwde synchronisatie- en vergrendelingsprimitieven van Java te gebruiken; bijv. het synchronized trefwoord. Het volgende voorbeeld laat zien hoe we synchronized kunnen gebruiken om tellingen te verzamelen.

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;
    }
}

Deze implementatie zal correct werken. Als u echter een groot aantal threads meerdere gelijktijdige oproepen op hetzelfde object Counters uitvoert, kan de synchronisatie een bottleneck zijn. Concreet:

  1. Elke synchronized methode-aanroep begint met de huidige thread die het slot voor de instantie Counters .
  2. De draad zal het slot vast te houden terwijl het controleert number waarde en werkt de teller.
  3. Ten slotte zal het het slot ontgrendelen, waardoor andere threads toegang krijgen.

Als een thread probeert het slot te verkrijgen terwijl een andere het vasthoudt, wordt de poging tot thread geblokkeerd (gestopt) in stap 1 totdat het slot wordt vrijgegeven. Als er meerdere threads wachten, krijgt een van hen deze en blijven de andere geblokkeerd.

Dit kan tot een aantal problemen leiden:

  • Als er veel twist is over het slot (dat wil zeggen dat veel threads proberen het te verkrijgen), kunnen sommige threads lange tijd worden geblokkeerd.

  • Wanneer een thread wordt geblokkeerd in afwachting van het slot, zal het besturingssysteem meestal proberen de uitvoering naar een andere thread te schakelen. Deze context switching loopt een relatief grote invloed op de prestaties van de processor.

  • Wanneer er meerdere threads op hetzelfde slot zijn geblokkeerd, zijn er geen garanties dat een van hen "redelijk" zal worden behandeld (dat wil zeggen dat elke thread is gepland om te worden uitgevoerd). Dit kan leiden tot uithongering van de draad .

Hoe implementeer je atoomtypen?

Laten we beginnen met het herschrijven van het bovenstaande voorbeeld met behulp van AtomicInteger tellers:

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;
    }
}

We hebben de int[] door een AtomicInteger[] en geïnitialiseerd met een instantie in elk element. We hebben ook aanroepen toegevoegd aan incrementAndGet() en get() in plaats van bewerkingen op int waarden.

Maar het belangrijkste is dat we het synchronized trefwoord kunnen verwijderen omdat vergrendeling niet langer nodig is. Dit werkt omdat de bewerkingen incrementAndGet() en get() atomair en thread-safe zijn . In deze context betekent dit dat:

  • Elke teller in de array kan alleen worden waargenomen in de "voor" -status voor een bewerking (zoals een "increment") of in de "na" -status.

  • Ervan uitgaande dat de bewerking plaatsvindt op tijdstip T , kan geen enkele thread de "vóór" -status na tijdstip T .

Bovendien, terwijl twee threads mogelijk tegelijkertijd proberen dezelfde AtomicInteger instantie tegelijkertijd bij te werken, zorgen de implementaties van de bewerkingen ervoor dat er slechts één stap per keer gebeurt op de gegeven instantie. Dit gebeurt zonder vergrendeling, wat vaak resulteert in betere prestaties.

Hoe werken atoomtypen?

Atoomtypen vertrouwen meestal op gespecialiseerde hardware-instructies in de instructieset van de doelmachine. Op Intel gebaseerde instructiesets bieden bijvoorbeeld een CAS -instructie ( Compare and Swap ) die een specifieke reeks geheugenbewerkingen atomisch uitvoert.

Deze instructies op laag niveau worden gebruikt om bewerkingen op hoger niveau in de API's van de respectieve AtomicXxx klassen te AtomicXxx . Bijvoorbeeld (nogmaals, in C-achtige pseudocode):

private volatile num;

int increment() {
  while (TRUE) {
    int old = num;
    int new = old + 1;
    if (old == compare_and_swap(&num, old, new)) {
      return new;
    }
  }
}

Als er geen discussie is over de AtomicXxxx , zal de if test slagen en wordt de lus onmiddellijk beëindigd. Als er twist is, dan zal de if falen voor alle behalve één van de threads, en ze zullen "draaien" in de lus gedurende een klein aantal cycli van de lus. In de praktijk is het spinnen sneller orde van grootte (behalve bij onrealistisch hoge conflictniveaus, waar gesynchroniseerd beter presteert dan atoomklassen omdat wanneer de CAS-bewerking mislukt, de nieuwe poging alleen meer contentie toevoegt) dan de thread opschorten en naar een andere overschakelen een.

Overigens worden CAS-instructies meestal door de JVM gebruikt om niet- vergrendelde vergrendeling te implementeren. Als de JVM kan zien dat een slot momenteel niet is vergrendeld, probeert het een CAS te gebruiken om het slot te verkrijgen. Als het CAS slaagt, is het niet nodig om de dure threadplanning, contextomschakeling, enzovoort te doen. Zie Bias Locking in HotSpot voor meer informatie over de gebruikte technieken.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow