Što je sigurnost navoja i kako to postići?

1. Pregled

Java podržava multithreading iz kutije. To znači da istodobno izvođenje bajt-koda u odvojenim radničkim nitima JVM može poboljšati izvedbu aplikacije.

Iako je multitreading moćna značajka, ima svoju cijenu. U višenitnim okruženjima, implementacije moramo pisati na nit-siguran način. To znači da različite niti mogu pristupiti istim resursima bez izlaganja pogrešnog ponašanja ili stvaranja nepredvidivih rezultata. Ova metodologija programiranja poznata je kao "zaštita od niti".

U ovom ćemo uputstvu razmotriti različite pristupe za njegovo postizanje.

2. Provedbe bez državljanstva

U većini slučajeva pogreške u višenitnim aplikacijama rezultat su pogrešnog dijeljenja stanja između nekoliko niti.

Stoga je prvi pristup koji ćemo razmotriti postići sigurnost niti koristeći implementacije bez državljanstva.

Da bismo bolje razumjeli ovaj pristup, razmotrimo jednostavnu klasu uslužnih programa sa statičkom metodom koja izračunava faktorijel broja:

javna klasa MathUtils {javni statički faktor BigInteger (int broj) {BigInteger f = novi BigInteger ("1"); za (int i = 2; i <= broj; i ++) {f = f.multiply (BigInteger.valueOf (i)); } povratak f; }} 

The faktorijel() metoda je deterministička funkcija bez stanja. S obzirom na određeni ulaz, on uvijek daje isti izlaz.

Metoda niti se oslanja na vanjsko stanje niti ga uopće održava. Stoga se smatra da je siguran za nit i može ga sigurno zvati više niti istovremeno.

Sve niti mogu sigurno nazvati faktorijel() metoda i dobit će očekivani rezultat bez međusobnog ometanja i bez mijenjanja rezultata koji metoda generira za druge niti.

Stoga, Implementacije bez državljanstva najjednostavniji su način postizanja sigurnosti niti.

3. Nepromjenjive implementacije

Ako trebamo dijeliti stanje između različitih niti, možemo stvoriti klase zaštićene nitima čineći ih nepromjenjivima.

Nepromjenjivost je moćan, jezično agnostički koncept i to je prilično lako postići u Javi.

Jednostavno rečeno, instanca klase je nepromjenjiva kada se njezino unutarnje stanje ne može mijenjati nakon što je konstruirano.

Najlakši način za stvaranje nepromjenjive klase u Javi je deklariranjem svih polja privatni i konačni a ne pružaju postavljače:

javna klasa MessageService {private final String message; javna MessageService (niz poruka) {this.message = message; } // standardni getter}

A MessageService objekt je učinkovito nepromjenjiv jer se njegovo stanje ne može promijeniti nakon njegove izgradnje. Dakle, siguran je u nitima.

Štoviše, ako MessageService zapravo bili promjenjivi, ali više niti ima pristup samo za čitanje, a također je i sigurno.

Tako, nepromjenjivost je samo još jedan način postizanja sigurnosti niti.

4. Lokalna polja s nitima

U objektno orijentiranom programiranju (OOP) objekti zapravo trebaju održavati stanje kroz polja i provoditi ponašanje pomoću jedne ili više metoda.

Ako zapravo moramo održavati stanje, možemo stvoriti klase zaštićene nitima koje ne dijele stanje između niti tako što ćemo njihova polja učiniti nitima lokalnima.

Jednostavno možemo stvoriti klase čija su polja lokalna niti jednostavnim definiranjem privatnih polja u Nit razreda.

Mogli bismo definirati, na primjer, a Nit klasa koja pohranjuje niz od cijeli brojevi:

javna klasa ThreadA proširuje Thread {privatni konačni brojevi popisa = Arrays.asList (1, 2, 3, 4, 5, 6); @Preuzmi javnu void run () {numbers.forEach (System.out :: println); }}

Dok bi drugi mogao držati niz od žice:

javna klasa ThreadB proširuje Thread {private final Popis slova = Arrays.asList ("a", "b", "c", "d", "e", "f"); @Preuzmi javno void run () {letters.forEach (System.out :: println); }}

U obje implementacije, klase imaju svoje stanje, ali se ne dijeli s drugim nitima. Dakle, klase su zaštićene niti.

Slično tome, dodjeljivanjem možemo stvoriti lokalno polje niti ThreadLocal instance na polje.

Razmotrimo, na primjer, sljedeće StateHolder razred:

public class StateHolder {private final String state; // standardni konstruktori / getter}

Lako ga možemo napraviti varijablom koja je lokalna za nit na sljedeći način:

javna klasa ThreadState {javni statički konačni ThreadLocal statePerThread = new ThreadLocal () {@Override protected StateHolder initialValue () {return new StateHolder ("active"); }}; javni statični StateHolder getState () {return statePerThread.get (); }}

Lokalna polja s nitima prilično su slična poljima normalne klase, osim što svaka nit koja im pristupa putem postavljača / dobivača dobiva neovisno inicijaliziranu kopiju polja tako da svaka nit ima svoje stanje.

5. Sinkronizirane zbirke

Pomoću skupa omota za sinkronizaciju koji su uključeni u okvir zbirki možemo lako stvoriti zbirke zaštićene nitima.

Na primjer, možemo koristiti jedan od ovih omotača za sinkronizaciju za stvaranje kolekcije sigurne za nit:

Zbirka syncCollection = Collections.synchronizedCollection (novi ArrayList ()); Tema niti1 = nova nit (() -> syncCollection.addAll (Arrays.asList (1, 2, 3, 4, 5, 6))); Nit niti2 = nova nit (() -> syncCollection.addAll (Arrays.asList (7, 8, 9, 10, 11, 12))); thread1.start (); thread2.start (); 

Imajmo na umu da sinkronizirane zbirke koriste unutarnje zaključavanje u svakoj metodi (unutarnje zaključavanje ćemo pogledati kasnije).

To znači da metodama može pristupiti samo jedna nit odjednom, dok će druge niti biti blokirane dok metodu ne otključa prva nit.

Dakle, sinkronizacija ima kaznu u izvedbi zbog osnovne logike sinkroniziranog pristupa.

6. Istodobne zbirke

Kao alternativu sinkroniziranim zbirkama, istodobne kolekcije možemo koristiti za stvaranje kolekcija sigurnih u nitima.

Java nudi java.util.concurrent paket, koji sadrži nekoliko istodobnih zbirki, poput ConcurrentHashMap:

Karta concurrentMap = novo ConcurrentHashMap (); concurrentMap.put ("1", "jedan"); concurrentMap.put ("2", "dva"); concurrentMap.put ("3", "tri"); 

Za razliku od njihovih sinkroniziranih kolega, istodobne kolekcije postižu sigurnost niti dijeljenjem svojih podataka u segmente. U ConcurrentHashMap, na primjer, nekoliko niti može dobiti zaključavanje na različitim segmentima karte, tako da više niti može pristupiti Karta u isto vrijeme.

Istodobne zbirke sumnogo učinkovitiji od sinkroniziranih zbirki, zbog svojstvenih prednosti istodobnog pristupa niti.

Vrijedno je to spomenuti sinkronizirane i istodobne zbirke čine samo kolekciju sigurnom u nitima, a ne i sadržaj.

7. Atomski objekti

Također je moguće postići sigurnost niti koristeći skup atomskih klasa koje Java nudi, uključujući AtomicInteger, AtomicLong, AtomicBoolean, i AtomicReference.

Atomske klase omogućuju nam izvođenje atomskih operacija koje su zaštićene nitima bez upotrebe sinkronizacije. Atomska operacija se izvodi u jednoj operaciji na razini stroja.

Da bismo razumjeli problem koji ovo rješava, pogledajmo sljedeće Brojač razred:

brojač javne klase {private int counter = 0; public void incrementCounter () {brojač + = 1; } public int getCounter () {return brojač; }}

Pretpostavimo da u trkačkom stanju dvije niti pristupaju inkrementCounter () metoda u isto vrijeme.

U teoriji, konačna vrijednost brojač polje će biti 2. Ali jednostavno ne možemo biti sigurni u rezultat, jer niti istodobno izvršavaju isti blok koda, a priraštaj nije atomski.

Stvorimo nit implementaciju Brojač klase pomoću AtomicInteger objekt:

javna klasa AtomicCounter {privatni konačni brojač AtomicInteger = novi AtomicInteger (); javna praznina inkrementCounter () {counter.incrementAndGet (); } public int getCounter () {return counter.get (); }}

Ovo je sigurno bez niti, jer, dok je za povećanje, ++, potrebno više od jedne operacije, inkrementAndGet je atomska.

8. Sinkronizirane metode

Iako su raniji pristupi vrlo dobri za kolekcije i primitive, s vremena na vrijeme trebat će nam veća kontrola od toga.

Dakle, još jedan uobičajeni pristup koji možemo koristiti za postizanje sigurnosti niti je primjena sinkroniziranih metoda.

Jednostavno rečeno, samo jedna nit može istovremeno pristupiti sinkroniziranoj metodi dok blokira pristup ovoj metodi iz drugih niti. Ostale niti ostat će blokirane sve dok prva nit ne završi ili metoda izbaci iznimku.

Možemo stvoriti verziju sigurnu za nit inkrementCounter () na drugi način čineći ga sinkroniziranom metodom:

javna sinkronizirana praznina inkrementCounter () {brojač + = 1; }

Stvorili smo sinkroniziranu metodu dodavanjem prefiksa potpisu metode s sinkronizirano ključna riječ.

Budući da jedna po jedna nit može pristupiti sinkroniziranoj metodi, jedna će nit izvršiti inkrementCounter () metodom, a zauzvrat će i drugi to učiniti. Do izvršenja preklapanja neće doći.

Sinkronizirane metode oslanjaju se na upotrebu "unutarnjih brava" ili "brava za nadzor". Unutarnja brava je implicitni unutarnji entitet povezan s određenom instancom klase.

U kontekstu s više niti, pojam monitor je samo referenca na ulogu koju zaključavanje izvršava na pridruženom objektu, jer nameće ekskluzivni pristup skupu navedenih metoda ili izraza.

Kad nit pozove sinkroniziranu metodu, ona dobije unutarnju bravu. Nakon što nit završi s izvršavanjem metode, ona otpušta bravu, što omogućuje ostalim nitima da nabave bravu i dobiju pristup metodi.

Sinkronizaciju možemo implementirati u primjerima, statičkim metodama i izrazima (sinkronizirani izrazi).

9. Sinkronizirane izjave

Ponekad bi sinkronizacija cijele metode mogla biti pretjerana ako samo trebamo učiniti segment metode nitnim.

Da bismo ilustrirali ovaj slučaj upotrebe, refaktoriziramo inkrementCounter () metoda:

public void incrementCounter () {// dodatne sinkronizirane operacije sinkronizirane (ovo) {counter + = 1; }}

Primjer je trivijalan, ali pokazuje kako stvoriti sinkronizirani izraz. Pod pretpostavkom da metoda sada izvodi nekoliko dodatnih operacija, koje ne zahtijevaju sinkronizaciju, sinkronizirali smo samo relevantni odjeljak za modificiranje stanja umotavanjem u sinkronizirano blok.

Za razliku od sinkroniziranih metoda, sinkronizirani izrazi moraju navesti objekt koji pruža unutarnju bravu, obično ovaj referenca.

Sinkronizacija je skupa, pa s ovom opcijom možemo sinkronizirati samo relevantne dijelove metode.

9.1. Ostali predmeti kao brava

Možemo neznatno poboljšati implementaciju Brojač klasa iskorištavanjem drugog objekta kao brave monitora, umjesto ovaj.

To ne samo da omogućuje koordinirani pristup zajedničkom resursu u višenitnom okruženju, ali također koristi i vanjski entitet kako bi nametnuo ekskluzivni pristup resursu:

javna klasa ObjectLockCounter {private int counter = 0; privatno konačno zaključavanje objekta = novi objekt (); javna praznina inkrementCounter () {sinkronizirano (zaključavanje) {brojač + = 1; }} // standardni getter}

Koristimo običnu Objekt instancu za provedbu uzajamnog isključenja. Ova je implementacija nešto bolja jer promiče sigurnost na razini zaključavanja.

Prilikom korištenja ovaj za unutarnje zaključavanje, napadač bi mogao izazvati zastoj stjecanjem vlastite brave i pokretanjem stanja uskraćivanja usluge (DoS).

Naprotiv, kada koristite druge predmete, taj privatni entitet nije dostupan izvana. To napadaču otežava dobivanje brave i izaziva zastoj.

9.2. Upozorenja

Iako bilo koji Java objekt možemo koristiti kao unutarnju bravu, trebali bismo izbjegavati upotrebu Žice u svrhu zaključavanja:

javna klasa Class1 {private static final String LOCK = "Lock"; // koristi LOCK kao unutarnju bravu} javna klasa Class2 {private static final String LOCK = "Lock"; // koristi LOCK kao unutarnju bravu}

Na prvi pogled čini se da ove dvije klase koriste dva različita predmeta kao svoju bravu. Međutim, zbog interniranja niza, ove dvije vrijednosti "Lock" mogu se zapravo odnositi na isti objekt u spremištu nizova. Odnosno Razred1 i Razred2 dijele istu bravu!

To bi pak moglo uzrokovati neka neočekivana ponašanja u istodobnim kontekstima.

Pored Žice, trebali bismo izbjegavati upotrebu bilo kakvih predmeta koji se mogu predmemorirati ili višekratno koristiti kao unutarnje brave. Na primjer, Integer.valueOf () metoda sprema male brojeve. Stoga, pozivanje Integer.valueOf (1) vraća isti objekt čak i u različitim klasama.

10. Hlapljiva polja

Sinkronizirane metode i blokovi korisni su za rješavanje problema s promjenjivom vidljivošću među nitima. Uprkos tome, CPU može predmemorirati vrijednosti redovnih polja klase. Stoga naknadna ažuriranja određenog polja, čak i ako su sinkronizirana, možda neće biti vidljiva drugim nitima.

Da bismo spriječili ovu situaciju, možemo koristiti hlapljiv polja razreda:

brojač javne klase {private volatile int counter; // standardni konstruktori / getter}

Uz hlapljiv ključnu riječ, upućujemo JVM-u i kompajleru da pohrane brojač varijabla u glavnoj memoriji. Na taj način osiguravamo da svaki put kada JVM pročita vrijednost brojač varijabla, zapravo će je čitati iz glavne memorije, umjesto iz predmemorije CPU-a. Isto tako, svaki put kad JVM piše na brojač varijabla, vrijednost će biti zapisana u glavnu memoriju.

Štoviše, upotreba a hlapljiv varijabla osigurava da će se sve varijable koje su vidljive određenoj niti čitati i iz glavne memorije.

Razmotrimo sljedeći primjer:

korisnik javne klase {naziv privatnog niza; privatna hlapljiva doba; // standardni konstruktori / getteri}

U ovom slučaju, svaki put kada JVM napiše dobhlapljiv varijabla u glavnu memoriju, zapisat će trajno Ime varijabla i za glavnu memoriju. To osigurava da su najnovije vrijednosti obje varijable pohranjene u glavnoj memoriji, tako da će naknadna ažuriranja varijabli automatski biti vidljiva ostalim nitima.

Slično tome, ako nit čita vrijednost a hlapljiv varijabla, sve varijable vidljive niti bit će pročitane i iz glavne memorije.

Ovo prošireno jamstvo da hlapljiv varijable pružaju poznato je kao potpuno hlapljivo jamstvo vidljivosti.

11. Povratne brave

Java nudi poboljšani skup Zaključaj implementacije, čije je ponašanje nešto sofisticiranije od unutarnjih brava o kojima je ranije bilo riječi.

S vlastitim bravama, model stjecanja brave prilično je krut: jedna nit nabavi bravu, zatim izvršava metodu ili blok koda i na kraju otpušta bravu, tako da je druge niti mogu dobiti i pristupiti metodi.

Ne postoji osnovni mehanizam koji provjerava niti u redu i daje prioritetni pristup nitima s najdužim čekanjem.

ReentrantLock instance nam omogućuju upravo to, otuda sprečavajući niti u redu da trpe neke vrste izgladnjivanja resursa:

javna klasa ReentrantLockCounter {private int counter; privatni konačni ReentrantLock reLock = novi ReentrantLock (istina); javna praznina inkrementCounter () {reLock.lock (); probajte {brojač + = 1; } napokon {reLock.unlock (); }} // standardni konstruktori / getter}

The ReentrantLock konstruktor uzima neobavezno poštenjeboolean parametar. Kad se postavi na pravi, a više niti pokušava dobiti bravu, JVM će dati prioritet niti koja se najdulje čeka i odobriti pristup bravi.

12. Brave za čitanje / pisanje

Još jedan moćan mehanizam koji možemo koristiti za postizanje sigurnosti niti je upotreba ReadWriteLock implementacije.

A ReadWriteLock lock zapravo koristi par pridruženih brava, jednu za operacije samo za čitanje, a drugu za operacije pisanja.

Kao rezultat, moguće je imati puno niti koje čitaju resurs, sve dok u njega nema niti koja piše. Štoviše, upisivanje niti u resurs spriječit će druge niti da ga pročitaju.

Možemo koristiti a ReadWriteLock zaključajte kako slijedi:

javna klasa ReentrantReadWriteLockCounter {private int counter; privatni konačni ReentrantReadWriteLock rwLock = novi ReentrantReadWriteLock (); privatno konačno zaključavanje readLock = rwLock.readLock (); privatno konačno zaključavanje writeLock = rwLock.writeLock (); javna praznina inkrementCounter () {writeLock.lock (); probajte {brojač + = 1; } napokon {writeLock.unlock (); }} javni int getCounter () {readLock.lock (); try {return brojač; } napokon {readLock.unlock (); }} // standardni konstruktori} 

13. Zaključak

U ovom članku, naučili smo što je zaštita od niti u Javi i detaljno smo pogledali različite pristupe za njezino postizanje.

Kao i obično, svi uzorci koda prikazani u ovom članku dostupni su na GitHubu.