Mi a szálbiztonság és hogyan érhető el?

1. Áttekintés

A Java támogatja a többszálas szálkészítést. Ez azt jelenti, hogy a bytecode egyidejű futtatásával külön munkásszálakban a JVM képes javítani az alkalmazás teljesítményét.

Noha a többszálas szálak erős funkciók, ennek ára van. Többszálas környezetben szálbiztos módon kell megírnunk a megvalósításokat. Ez azt jelenti, hogy a különböző szálak ugyanazokhoz az erőforrásokhoz férhetnek hozzá, anélkül, hogy téves viselkedést tennének ki, vagy kiszámíthatatlan eredményeket hoznának létre. Ezt a programozási módszertant „menet-biztonságnak” nevezik.

Ebben az oktatóanyagban különböző megközelítéseket vizsgálunk meg az elérése érdekében.

2. Hontalan végrehajtások

A legtöbb esetben a többszálas alkalmazásokban előforduló hibák a több szál közötti helytelen állapotmegosztás eredményeként következnek be.

Ezért az első megközelítés, amelyet megvizsgálunk, a menetbiztonság elérése hontalan megvalósítások felhasználásával.

E megközelítés jobb megértése érdekében vegyünk egy egyszerű segédosztályt statikus módszerrel, amely kiszámítja a szám faktoriálját:

public class MathUtils {public static BigInteger faktoriális (int szám) {BigInteger f = new BigInteger ("1"); for (int i = 2; i <= szám; i ++) {f = f.többszörösen (BigInteger.valueOf (i)); } return f; }} 

A faktoriális() módszer hontalan determinisztikus függvény. Egy adott bemenetre való tekintettel mindig ugyanazt a kimenetet produkálja.

A módszer, a metódus sem a külső állapotra támaszkodik, sem pedig egyáltalán nem tartja fenn az állapotot. Ezért szálbiztosnak tekintik, és egyszerre több szál is biztonságosan hívhatja.

Minden szál biztonságosan hívhatja a faktoriális() módszerrel, és anélkül kapja meg a várt eredményt, hogy egymásba avatkoznának, és anélkül, hogy megváltoztatná a kimenetet, amelyet a módszer más szálakhoz generál.

Ebből kifolyólag, hontalan megvalósítások a szálbiztonság legegyszerűbb módja.

3. Megváltozhatatlan megvalósítások

Ha meg kell osztanunk az állapotot a különböző szálak között, akkor létrehozhatunk szálbiztos osztályokat azáltal, hogy megváltoztathatatlanná teszik őket.

A megváltoztathatatlanság egy erőteljes, nyelv-agnosztikus fogalom, amelyet Java-ban meglehetősen könnyű megvalósítani.

Egyszerűen szólva, egy osztálypéldány megváltoztathatatlan, ha belső állapota a szerkesztés után nem módosítható.

A Java-ban egy megváltoztathatatlan osztály létrehozásának legegyszerűbb módja az összes mező deklarálása magán és végső és nem biztosítanak beállítókat:

public class MessageService {private final String üzenet; public MessageService (String üzenet) {this.message = üzenet; } // standard getter}

A MessageService az objektum gyakorlatilag megváltoztathatatlan, mivel az állapota az építése után nem változhat. Ezért szálbiztos.

Sőt, ha MessageService valójában mutábilisak voltak, de több szál csak írásvédett hozzáféréssel rendelkezik, ez szálbiztos is.

Így, a változtathatatlanság csak egy másik módja a menetbiztonság elérésének.

4. Menet-lokális mezők

Az objektum-orientált programozásban (OOP) az objektumoknak valójában fenn kell tartaniuk az állapotot a mezőkön keresztül, és a viselkedést egy vagy több módszerrel kell megvalósítaniuk.

Ha valóban fenn kell tartanunk az állapotot, olyan szálbiztonsági osztályokat hozhatunk létre, amelyek nem osztják meg az állapotot a szálak között, ha mezőiket szál-lokálissá teszik.

Könnyen létrehozhatunk olyan osztályokat, amelyek mezői szál-lokálisak, egyszerűen meghatározva a privát mezőket cérna osztályok.

Meghatározhatnánk például a cérna osztály, amely egy sor nak,-nek egész számok:

a ThreadA nyilvános osztály kiterjeszti a Thread-t {private final List number = Arrays.asList (1, 2, 3, 4, 5, 6); @Orride public void run () {numbers.forEach (System.out :: println); }}

Míg egy másiknak lehet egy sor nak,-nek húrok:

a ThreadB nyilvános osztály kiterjeszti a Thread {privát végső lista betűit = tömbök.asList ("a", "b", "c", "d", "e", "f"); @Orride public void run () {letters.forEach (System.out :: println); }}

Mindkét megvalósításban az osztályoknak megvan a saját állapotuk, de nem osztják meg más szálakkal. Így az osztályok szálbiztosak.

Hasonlóképpen hozzárendeléssel létrehozhatunk szál-helyi mezőket is ThreadLocal példányok egy mezőre.

Vegyük fontolóra például a következőket StateHolder osztály:

public class StateHolder {private final String állapot; // standard konstruktorok / getter}

Könnyen szál-lokális változóvá tehetjük az alábbiak szerint:

public class ThreadState {public static final ThreadLocal statePerThread = new ThreadLocal () {@Orride protected StateHolder initialValue () {return new StateHolder ("active"); }}; public static StateHolder getState () {return statePerThread.get (); }}

A szál-lokális mezők nagyjából hasonlítanak a normál osztálymezőkhöz, azzal a különbséggel, hogy minden szál, amely hozzájuk fér hozzá egy szetter / getter útján, a mező független inicializált másolatát kapja, így minden szálnak megvan a maga állapota.

5. Szinkronizált gyűjtemények

Könnyen létrehozhatunk szálbiztos gyűjteményeket a szinkronizációs csomagolók használatával, amelyek a gyűjtemény keretrendszerben találhatók.

Például az egyik ilyen szinkronizáló burkoló segítségével létrehozhatunk egy szálbiztos gyűjteményt:

Gyűjtemény syncCollection = Collections.synchronizedCollection (új ArrayList ()); Thread thread1 = new Thread (() -> syncCollection.addAll (Arrays.asList (1, 2, 3, 4, 5, 6))); Menetszál2 = új Szál (() -> syncCollection.addAll (Tömbök.asList (7, 8, 9, 10, 11, 12))); thread1.start (); thread2.start (); 

Ne felejtsük el, hogy a szinkronizált gyűjtemények belső zárolást használnak az egyes módszerekben (a belső zárolást később megvizsgáljuk).

Ez azt jelenti, hogy a módszerekhez egyszerre csak egy szál férhet hozzá, míg a többi szál blokkolva lesz, amíg a módszert az első szál fel nem oldja.

Így a szinkronizálásnak büntetése van a teljesítményben, a szinkronizált hozzáférés mögöttes logikája miatt.

6. Egyidejű gyűjtemények

A szinkronizált gyűjtemények helyett egyidejű gyűjteményeket is használhatunk szálbiztos gyűjtemények létrehozására.

A Java biztosítja a java.util.egyidejű csomag, amely több egyidejű gyűjteményt tartalmaz, például ConcurrentHashMap:

Map concurrentMap = new ConcurrentHashMap (); concurrentMap.put ("1", "egy"); concurrentMap.put ("2", "kettő"); concurrentMap.put ("3", "három"); 

Ellentétben szinkronizált társaikkal, az egyidejű gyűjtemények szálbiztonságot érnek el azáltal, hogy adataikat szegmensekre osztják. A ConcurrentHashMap, például több szál megszerezheti a zárakat a különböző térképszegmensekben, így több szál is hozzáférhet a Térkép ugyanabban az időben.

Egyidejű gyűjteményeksokkal teljesítőbb, mint a szinkronizált gyűjtemények, az egyidejű szálhozzáférés eredendő előnyei miatt.

Érdemes ezt megemlíteni a szinkronizált és egyidejű gyűjtemények csak magát a gyűjteményt teszik szálbiztonságossá, a tartalmat nem.

7. Atomtárgyak

Az is lehetséges, hogy szálbiztonságot érjünk el a Java által biztosított atomosztályok használatával, beleértve AtomicInteger, AtomicLong, AtomicBoolean, és AtomicReference.

Az atomosztályok lehetővé teszik számunkra az atomműveletek végrehajtását, amelyek szálbiztosak, szinkronizálás nélkül. Egy atomi műveletet egyetlen gépi szintű művelettel hajtanak végre.

A megoldandó probléma megértéséhez nézzük meg a következőket Számláló osztály:

public class Counter {private int counter = 0; public void incrementCounter () {számláló + = 1; } public int getCounter () {return counter; }}

Tegyük fel, hogy versenyhelyzetben két szál fér hozzá a incrementCounter () módszer egyidejűleg.

Elméletileg a számláló mező értéke 2. De egyszerűen nem lehetünk biztosak az eredményben, mert a szálak ugyanazt a kódblokkot hajtják végre egyszerre, és az inkrementáció nem atom.

Hozzunk létre egy szálbiztos megvalósítást a Számláló osztály egy an használatával AtomicInteger tárgy:

public class AtomicCounter {private final AtomicInteger counter = new AtomicInteger (); public void incrementCounter () {counter.incrementAndGet (); } public int getCounter () {return counter.get (); }}

Ez szálbiztos, mert míg a ++ növekmény több műveletet igényel, incrementAndGet atomatom.

8. Szinkronizált módszerek

Míg a korábbi megközelítések nagyon jók a gyűjtemények és a primitívek számára, időnként ennél nagyobb ellenőrzésre lesz szükségünk.

Tehát egy másik általános megközelítés, amelyet a szálbiztonság elérésére használhatunk, a szinkronizált módszerek megvalósítása.

Egyszerűen fogalmazva, egyszerre csak egy szál férhet hozzá egy szinkronizált módszerhez, miközben blokkolja a metódus hozzáférését más szálakból. Más szálak blokkolva maradnak, amíg az első szál befejeződik, vagy a módszer kivételt nem vet.

Készíthetünk egy szálbiztos verziót incrementCounter () más módon szinkronizált módszerré téve:

public synchronized void incrementCounter () {számláló + = 1; }

Létrehoztunk egy szinkronizált metódust úgy, hogy a metódus aláírását a szinkronizált kulcsszó.

Mivel egy-egy szál elérheti a szinkronizált módszert, egy szál végrehajtja a incrementCounter () módszerrel, és ezt mások is megteszik. Nem történik átfedéses végrehajtás.

A szinkronizált módszerek „belső zárak” vagy „monitor zárak” használatán alapulnak. A belső zár egy implicit belső entitás, amely egy adott osztálypéldányhoz van társítva.

Többszálas kontextusban a kifejezés monitor csak hivatkozás arra a szerepre, amelyet a zár végrehajt a társított objektumon, mivel kizárólagos hozzáférést biztosít a megadott módszerek vagy utasítások halmazához.

Amikor egy szál szinkronizált módszert hív, megszerzi a belső zárat. Miután a szál befejezte a módszer végrehajtását, felszabadítja a zárat, így más szálak megszerezhetik a zárat, és hozzáférhetnek a módszerhez.

Megvalósíthatjuk a szinkronizálást példamódszerekben, statikus módszerekben és utasításokban (szinkronizált utasítások).

9. Szinkronizált nyilatkozatok

Előfordulhat, hogy egy teljes módszer szinkronizálása túlterhelő lehet, ha csak a módszer egy szakaszát kell szálbiztonságossá tennünk.

Hogy ezt a felhasználási esetet példázzuk, alakítsuk át a incrementCounter () módszer:

public void incrementCounter () {// további szinkronizálatlan műveletek szinkronizálva (ez) {számláló + = 1; }}

A példa triviális, de megmutatja, hogyan hozhatunk létre szinkronizált utasítást. Feltételezve, hogy a módszer most elvégez néhány további műveletet, amelyek nem igényelnek szinkronizálást, csak a megfelelő állapotmódosító szakaszt szinkronizáltuk egy szinkronizált Blokk.

A szinkronizált módszerektől eltérően a szinkronizált utasításoknak meg kell adniuk a belső zárat biztosító objektumot, általában a ez referencia.

A szinkronizálás drága, ezért ezzel az opcióval csak egy módszer releváns részeit tudjuk szinkronizálni.

9.1. Egyéb objektumok zárként

Kicsit javíthatjuk a Számláló osztály egy másik objektum monitor zárként történő kihasználásával, ahelyett, hogy ez.

Ez nemcsak összehangolt hozzáférést biztosít egy megosztott erőforráshoz többszálas környezetben, hanem külső entitást is alkalmaz az erőforráshoz való kizárólagos hozzáférés kikényszerítésére:

public class ObjectLockCounter {private int counter = 0; private final Object lock = new Object (); public void incrementCounter () {szinkronizált (lock) {számláló + = 1; }} // standard getter}

Egy síkságot használunk Tárgy például a kölcsönös kirekesztés érvényesítése érdekében. Ez a megvalósítás valamivel jobb, mivel elősegíti a biztonságot zárolási szinten.

Használat során ez belső reteszeléshez, a támadó holtpontot okozhat a belső zár megszerzésével és a szolgáltatás megtagadásának (DoS) feltételével.

Éppen ellenkezőleg, más objektumok használatakor hogy a magánvállalkozás kívülről nem hozzáférhető. Ez megnehezíti a támadók számára a zár megszerzését és holtpont kialakulását.

9.2. Figyelmeztetések

Annak ellenére, hogy bármilyen Java objektumot használhatunk belső zárként, kerülnünk kell a használatát Húrok reteszelés céljából:

public class 1. osztály {private static final String LOCK = "Lock"; // a LOCK-ot használja belső zárként} public class2 class {private static final String LOCK = "Lock"; // a LOCK-ot belső zárként használja}

Első pillantásra úgy tűnik, hogy ez a két osztály két különböző objektumot használ záraként. Azonban, a karakterlánc internálás miatt ez a két „Lock” érték valójában ugyanarra az objektumra utalhat a karakterlánc készletben. Ez a 1. osztály és 2. osztály ugyanazt a zárat használják!

Ez viszont váratlan viselkedést okozhat egyidejű összefüggésekben.

Továbbá Húrok, kerülnünk kell bármilyen gyorsítótárazott vagy újrafelhasználható objektumot belső zárként használni. Például a Integer.valueOf () módszer kis számban tárolja a gyorsítótárat. Ezért hívás Integer.valueOf (1) ugyanazt az objektumot adja vissza különböző osztályokban is.

10. Illékony mezők

A szinkronizált módszerek és blokkok hasznosak a szálak közötti változó láthatósági problémák kezelésében. Ennek ellenére a normál osztálymezők értékeit a CPU tárolhatja. Ezért előfordulhat, hogy egy adott mező következményes frissítései, még ha szinkronizálva is vannak, nem láthatók más szálak számára.

Ennek a helyzetnek a megelőzésére használhatjuk illó osztály mezők:

public class Counter {private volatile int counter; // standard konstruktorok / getter}

A ... val illó kulcsszóval utasítjuk a JVM-et és a fordítót a számláló változó a fő memóriában. Így gondoskodunk arról, hogy minden alkalommal, amikor a JVM beolvassa a számláló változó, valójában a fő memóriából fogja olvasni, a CPU gyorsítótár helyett. Hasonlóképpen, valahányszor a JVM ír a számláló változó esetén az érték a fő memóriába kerül.

Ráadásul, a használata illó változó biztosítja, hogy minden változó, amely egy adott szál számára látható, a fő memóriából is olvasható legyen.

Vizsgáljuk meg a következő példát:

public class Felhasználó {private String name; magán ingatag int kor; // szabványos kivitelezők / szerelők}

Ebben az esetben a JVM minden alkalommal megírja a korilló változó a fő memóriához, akkor a nem felejtőt írja név változó a fő memóriához is. Ez biztosítja, hogy mindkét változó legújabb értékei a fő memóriában vannak tárolva, így a változók következetes frissítései automatikusan láthatók lesznek a többi szál számára.

Hasonlóképpen, ha egy szál beolvassa az a értékét illó változó, a szál számára látható összes változó ki lesz olvasva a fő memóriából is.

Ez a kiterjesztett garancia arra illó a változókat a teljes illékony láthatósági garancia néven ismerjük.

11. Visszatérő zárak

A Java továbbfejlesztett készletet kínál Zár megvalósítások, amelyek viselkedése valamivel kifinomultabb, mint a fent tárgyalt belső zárak.

Belső zárakkal a zárszerzési modell meglehetősen merev: az egyik szál megszerzi a zárat, majd végrehajt egy metódust vagy kódblokkot, végül felszabadítja a zárat, így más szálak megszerezhetik és elérhetik a módszert.

Nincs olyan mögöttes mechanizmus, amely ellenőrzi a sorban lévő szálakat, és elsőbbséget biztosít a leghosszabb várakozási szálakhoz.

ReentrantLock példák lehetővé teszik számunkra, hogy pontosan ezt tegyük, ennélfogva megakadályozza, hogy a sorban lévő szálak bizonyos típusú erőforrások éhezését szenvedjék:

nyilvános osztály ReentrantLockCounter {private int counter; privát végső ReentrantLock reLock = új ReentrantLock (igaz); public void incrementCounter () {reLock.lock (); próbáld ki a {számláló + = 1; } végül {reLock.unlock (); }} // szabványos kivitelezők / getter}

A ReentrantLock a kivitelező választható méltányosságlogikai paraméter. Ha beállítva igaz, és több szál próbál zárat szerezni, a JVM elsőbbséget biztosít a leghosszabb várakozási szálnak, és hozzáférést biztosít a zárhoz.

12. Zárak olvasása / írása

A másik hatékony mechanizmus, amelyet a szálbiztonság elérésére használhatunk, a használata ReadWriteLock megvalósítások.

A ReadWriteLock A lock valójában pár társított zárat használ, egyet csak olvasható műveletekhez, másikat írási műveletekhez.

Ennek eredményeként lehetséges, hogy sok szál olvasson egy erőforrást, feltéve, hogy nincs hozzá szál írás. Ezenkívül az erőforráshoz írt szál megakadályozza, hogy más szálak elolvassák.

Használhatjuk a ReadWriteLock zár az alábbiak szerint:

public class ReentrantReadWriteLockCounter {private int counter; privát végleges ReentrantReadWriteLock rwLock = új ReentrantReadWriteLock (); privát zár zár readLock = rwLock.readLock (); private final Lock writeLock = rwLock.writeLock (); public void incrementCounter () {writeLock.lock (); próbáld ki a {számláló + = 1; } végül {writeLock.unlock (); }} public int getCounter () {readLock.lock (); próbáld ki {return counter; } végül {readLock.unlock (); }} // szabványos kivitelezők} 

13. Következtetés

Ebben a cikkben, megtudtuk, mi a szálbiztonság a Java-ban, és alaposan megvizsgáltuk az elérésének különféle megközelítéseit.

Szokás szerint a cikkben bemutatott összes kódminta elérhető a GitHubon.