Ebben a leckében általánosságban a java.lang.ThreadLocal<> osztállyal való munkáról és annak többszálú környezetben való használatáról fogunk beszélni.

A ThreadLocal osztály változók tárolására szolgál. Ennek az osztálynak az a megkülönböztető jellemzője, hogy minden egyes, ezt használó szálhoz megőrzi egy érték külön független másolatát.

Az osztály működésébe mélyebben beleásva elképzelhetünk egy olyan Map-et , amely a szálakat olyan értékekre képezi le, amelyekből az aktuális szál veszi a megfelelő értéket, amikor használnia kell.

ThreadLocal osztálykonstruktor

Konstruktőr Akció
ThreadLocal() Üres változót hoz létre a Java-ban

Mód

Módszer Akció
kap() Az aktuális szál helyi változójának értékét adja vissza
készlet() Beállítja az aktuális szál helyi változójának értékét
eltávolítás() Eltávolítja az aktuális szál helyi változójának értékét
ThreadLocal.withInitial() További gyári módszer, amely beállítja a kezdeti értéket

felkészülni()

Írjunk egy példát, ahol két számlálót hozunk létre. Az első, egy közönséges változó, a szálak számának számlálására szolgál. A másodikat egy ThreadLocal- ba csomagoljuk . És meglátjuk, hogyan működnek együtt. Először írjunk egy ThreadDemo osztályt, amely örökli a Runnable-t , és tartalmazza az adatainkat, valamint a minden szempontból fontos run() metódust. Hozzáadunk egy módszert a számlálók képernyőn történő megjelenítéséhez:


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<>();

    public void run() {
        counter++;

        if(threadLocalCounter.get() != null) {
            threadLocalCounter.set(threadLocalCounter.get() + 1);
        } else {
            threadLocalCounter.set(0);
        }
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

Osztályunk minden egyes futásával növeljük aszámlálóváltozó hívja a get() metódust, hogy adatokat kapjon a ThreadLocal változóból. Ha az új szálban nincs adat, akkor 0-ra állítjuk. Ha van adat, növeljük eggyel. És írjuk le a módszerünket:


public static void main(String[] args) {
    ThreadDemo threadDemo = new ThreadDemo();

    Thread t1 = new Thread(threadDemo);
    Thread t2 = new Thread(threadDemo);
    Thread t3 = new Thread(threadDemo);

    t1.start();
    t2.start();
    t3.start();

}

Osztályunkat futtatva azt látjuk, hogy a ThreadLocal változó ugyanaz marad, függetlenül attól, hogy melyik szál éri el, de a szálak száma nő.

Számláló: 1
Számláló: 2
Számláló: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

A folyamat befejeződött 0 kilépési kóddal

eltávolítás()

Az eltávolítási módszer működésének megértéséhez csak kissé módosítjuk a kódot a ThreadDemo osztályban:


if(threadLocalCounter.get() != null) {
      threadLocalCounter.set(threadLocalCounter.get() + 1);
  } else {
      if (counter % 2 == 0) {
          threadLocalCounter.remove();
      } else {
          threadLocalCounter.set(0);
      }
  }

Ebben a kódban, ha a szálszámláló páros szám, akkor a ThreadLocal változónkban a remove() metódust hívjuk meg . Eredmény:

Számláló: 3
threadLocalCounter: 0
Számláló: 2
threadLocalCounter: null
Számláló: 1
threadLocalCounter: 0

A folyamat befejeződött 0 kilépési kóddal

És itt könnyen láthatjuk, hogy a második szál ThreadLocal változója null .

ThreadLocal.withInitial()

Ez a módszer létrehoz egy szál helyi változót.

A ThreadDemo osztály megvalósítása :


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 1);

    public void run() {
        counter++;
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

És megnézhetjük a kódunk eredményét:

Számláló: 1
Számláló: 2
Számláló: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

A folyamat befejeződött 0 kilépési kóddal

Miért használjunk ilyen változókat?

A ThreadLocal absztrakciót biztosít a helyi változók felett a java.lang.Thread végrehajtási szálhoz képest.

A ThreadLocal változók abban különböznek a hagyományos változóktól, hogy minden szálnak megvan a saját, egyedileg inicializált változata, amely a get() és set() metódusokon keresztül érhető el.

Minden szálhoz, azaz a Thread osztály példányához tartozik ThreadLocal változók térképe . A térkép kulcsai a ThreadLocal objektumokra, az értékek pedig a „megszerzett” ThreadLocal változókra hivatkoznak .

Miért nem alkalmas a Random osztály véletlen számok generálására többszálú alkalmazásokban?

A Random osztályt használjuk a véletlen számok meghatározásához. De ugyanilyen jól működik többszálú környezetben is? Ami azt illeti, nem. A Random nem alkalmas többszálú környezetekhez, mert ha több szál egyszerre ér el egy osztályt, a teljesítmény csökken.

A probléma megoldására a JDK 7 bevezette a java.util.concurrent.ThreadLocalRandom osztályt, amely véletlen számokat generál többszálú környezetben. Két osztályból áll: ThreadLocal és Random .

Az egyik szál által kapott véletlen számok függetlenek a többi száltól, de a java.util.Random globálisan véletlen számokat biztosít. Ezenkívül, a Random -tól eltérően , a ThreadLocalRandom nem támogatja az explicit vetést. Ehelyett felülírja a Random -ból örökölt setSeed() metódust , így hívásakor mindig UnsupportedOperationException- t dob.

Nézzük meg a ThreadLocalRandom osztály metódusait:

Módszer Akció
ThreadLocalVéletlenszerű aktuális() Az aktuális szál ThreadLocalRandom értékét adja vissza.
int next (int bit) Létrehozza a következő pszeudo-véletlen számot.
double nextDouble(dupla legkisebb, dupla korlát) Egy pszeudovéletlen számot ad vissza a legkisebb (befogadó) és a kötött (kizáró) közötti egyenletes eloszlásból .
int nextInt (legalább int, int kötött) Egy pszeudovéletlen számot ad vissza a legkisebb (befogadó) és a kötött (kizáró) közötti egyenletes eloszlásból.
hosszú következő Hosszú (hosszú n) A 0 (beleértve) és a megadott érték (kizárólag) közötti egyenletes eloszlásból származó álvéletlen számot ad vissza.
hosszú következőHosszú (hosszú legkevesebb, hosszú kötés) Egy pszeudovéletlen számot ad vissza a legkisebb (befogadó) és a kötött (kizáró) közötti egyenletes eloszlásból.
üres készletMag (hosszú mag) UnsupportedOperationExceptiont dob . Ez a generátor nem támogatja a vetést.

Véletlen számok lekérése a ThreadLocalRandom.current() segítségével

A ThreadLocalRandom a ThreadLocal és a Random osztályok kombinációja. Jobb teljesítményt ér el többszálú környezetben, egyszerűen elkerülve a Random osztály példányaihoz való egyidejű hozzáférést.

Valósítsunk meg egy példát, amely több szálat tartalmaz, és nézzük meg, hogyan működik az alkalmazásunk a ThreadLocalRandom osztállyal:


import java.util.concurrent.ThreadLocalRandom;

class RandomNumbers extends Thread {

    public void run() {
        try {
            int bound = 100;
            int result = ThreadLocalRandom.current().nextInt(bound);
            System.out.println("Thread " + Thread.currentThread().getId() + " generated " + result);
        }
        catch (Exception e) {
            System.out.println("Exception");
        }
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

				for (int i = 0; i < 10; i++) {
            RandomNumbers randomNumbers = new RandomNumbers();
            randomNumbers.start();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime));
    }
}

Programunk eredménye:

Eltelt idő: 1
Szál 17 generált 13
Szál 18 generált 41
Szál 16 generált 99
Szál 19 generált 25
Szál 23 generált 33
Szál 24
generált 21 Szál 15 generált 15
Szál 21
generált 28
Thread generált 39222

És most változtassuk meg a RandomNumbers osztályunkat, és használjuk a Random-ot benne:


int result = new Random().nextInt(bound);
Eltelt idő: 5
Szál 20 generált 48
Szál 19 generált 57
Szál 18 generált 90
Szál 22 generált 43
Szál 24 generált 7
Szál 23 generált 63
Szál 15 generált 2
szál 16 generált 40
szál 217 Thread genered 40
Thread 217

Írd fel! Tesztjeink során az eredmények néha megegyeztek, néha pedig eltérőek voltak. De ha több szálat használunk (mondjuk 100-at), az eredmény így fog kinézni:

Véletlen – 19-25 ms
ThreadLocalVéletlenszerű – 17-19 ms

Ennek megfelelően minél több szál van az alkalmazásunkban, annál nagyobb a teljesítmény, ha a Random osztályt többszálú környezetben használjuk.

Összefoglalva és megismételve a Random és a ThreadLocalRandom osztályok közötti különbségeket:

Véletlen ThreadLocalRandom
Ha különböző szálak ugyanazt a Random példányt használják , konfliktusok lépnek fel, és a teljesítmény csökken. Nincsenek ütközések vagy problémák, mert a generált véletlen számok az aktuális szál helyiek.
Lineáris kongruenciális képletet használ a kezdeti érték megváltoztatásához. A véletlenszám-generátor inicializálása egy belsőleg generált mag segítségével történik.
Hasznos olyan alkalmazásokban, ahol minden szál a saját véletlenszerű objektumkészletét használja. Hasznos olyan alkalmazásokban, ahol több szál párhuzamosan használ véletlen számokat a szálkészletekben.
Ez egy szülői osztály. Ez egy gyerekosztály.