Java-szálak

A Java nyelvi környezet különösen alkalmas többszálú programok készítésére, futtatására.

Szálak használata

A Java programozási nyelvben az explicit szálkezelés tipikusan a következő célok elérését szolgálja:

  • a program gyorsítása, oly módon, hogy a teendők egy-egy szálba vagy taszkba rendezhetők, így a megfelelő ütemezés mellett a program futási ideje rövidülhet
  • szálak átmeneti felfüggesztése
  • szálak összehangolása

Az aktuális szál felfüggesztése például a Thread osztály sleep metódusa meghívásával történhet, melynek argumentumként szál/program felfüggesztésének idejét kell megadni ezredmásodpercekben.

Pl: Thread.sleep(1000); Itt az adott szál futása 1 másodpercig lesz felfüggesztve.

Szálak létrehozása

Egy szál létrehozásának két lehetséges módja van:

  • Örököltethető a Thread osztályból az új osztály, melynek a run metódust kell felüldefiniálni,
  • Létrehozható a Runnable interfész implementálásával, amit át kell adni a Thread konstruktorának, ezután már el lehet indítani szálat a start metódussal.

Példa ez utóbbira:

  public class TestTask implements Runnable {
    public void run () {
      System.out.println("Helló világ");
    }

    public static void main (String[] args) {
      Thread thread = new Thread(new TestTask());
      thread.start();
    }
  }

Szinkronizáció

Egy többszálú Java-program egyszerre számos feladatot tud végrehajtani, függetlenül a processzormagok számától. A programok futtatásáért felelős JVM (Java virtuális gép, az angol Java Virtual Machine rövidítése) a saját ütemezője szerint választja ki, hogy éppen melyik szálat futtatja. Mindez JVM-implementáció függő, és befolyásolható több módon is:

  • A Thread.sleep(); utasítással várakoztathatók (időzíthetők) a különböző szálak. Például: két vagy több szálat használó program esetén, hogy a szálakat ne zavarják egymást. Az egyiket (vagy a többit) elaltatjuk annyi időre, hogy akkor "ébredjen(ek) fel", mikor a másik befejezte munkáját.
  • Vannak olyan helyzetek, mikor azt szeretnénk, hogy egy objektumot a konzisztencia érdekében egyszerre csak egy szál használhasson. Ez a synchronized kulcsszó használatával valósítható meg. Az objektum szinkronizálható egy metódushívás vagy egy utasításblokk idejére.

Az alábbi példában a saját objektum blokkolódik más szálak számára a findByName metódus végrehajtása alatt:

private final List<Item> items;

public synchronized Optional<Item> findByName(String name) {
    return items.stream()
            .filter(i -> i.getName(name).equals(name))
            .findAny();
}

Mivel a fenti items lista nem szinkronizált, fontos lehet, hogy explicite kizárjuk, hogy a különböző szálak egyszerre, egymást zavarva végezzenek műveleteket (különösen, ha egyes szálak módosíthatják is az adatokat).

A fenti módszerrel csak a saját objektum (statikus metódus esetén a Class<T> objektum, tehát például egy Foo nevű osztály esetén a Foo.class) blokkolható.

A blokkszintű (blokk alatt itt kódblokkot és nem a blokkolási mechanizmust értve) szinkronizáció esetében viszont explicite meg kell adni egy blokkolandó objektumot (monitor).

Optional<String> optionalItem;
synchronized (items) {
    optionalItem = items.stream()
            .filter(i -> i.getName(name).equals(name))
            .findAny();
}

A this kulcsszó használandó a saját objektum blokkolásához.

A szinkronizáció a memóriaszervezést is érinti. A JVM minden szál számára saját memória-gyorsítótárat tart fenn. A szinkronizált programrészek végrehajtása alatt minden adatmódosítás kizárólag a gyorsítótárat érinti, kivéve ha az adott objektum volatilis, azaz nem gyorstárazható, mely esetben nem szükséges blokkolás, ha csak kiolvasás történik. Bármely mező volatilissé tehető a volatile kulcsszóval, illetve egyes mezők automatikusan volatilisek (például ha a mező final). Azonos monitorra szinkronizált kódrészletek tehát közel úgy tekinthetők, mintha mindig egy közös szálon futnának.

A synchronized blokkok nem elég körültekintő használata ugyanúgy okozhat deadlockot, mint az explicitebb blokkolási mechanizmusok. Szándékosan is könnyen létrehozhatunk deadlock állapotot, ha két külön szálon egyszerre hívunk meg két olyan (lehetőleg viszonylag hosszabb ideig futó) metódust, melyek ugyanarra a két monitorra tartalmaznak egymásba ágyazott synchronized blokkokat, de ellenkező sorrendben, ekkor ugyanis a két szál olyan állapotban ragad, hogy egymásra várnak.

Új lehetőségek

A Java az 1.5 verziótól további szinkronizációs lehetőségeket kínál, nem nyelvi szinten, hanem új osztálykönyvtár biztosításával:

  • Task Scheduling Framework: az Executorok segítségével jobban szervezhetők például az opcionálisan időzített szálak
  • Concurrent Collections: a szokásos gyűjteménytípusok szálbiztos változatait tartalmazza
  • Atomic Variables: lehetőség összetett atomi műveleteket támogató változók használatára (pl. növelés és lekérdezés; összehasonlítás és beállítás)
  • Synchronizers: a szálak közötti koordinációt segítő osztályok (Semaphor, Mutex, Barrier, Latch és Exchanger)
  • Locks: explicit lockolási lehetőségek
  • Nanosecond-granularity timing: nanoszekudumos pontosságú időmérés, időzítés lehetősége

A Java 18 (egyelőre kísérleti jelleggel) bevezette a virtuális szálak (Virtual Threads) támogatását. Ezek olyan Java-szálak, melyekhez nem tartozik kernel-szintű szál, így (más előnyök mellett) kisebb overheaddel használhatók. A virtuális szálak is a Thread osztály leszármazottai.

Példa virtuális szál létrehozására:

ThreadFactory virtualThreadFactory = Thread
        .builder()
        .virtual()
        .factory();
Thread virtualThread = virtualThreadFactory
        .newThread(() -> System.out.println("Helló virtuális világ!"));
virtualThread.start();

Strategi Solo vs Squad di Free Fire: Cara Menang Mudah!