5000 reči i 10 slika koje jasno objašnjavaju ThreadLocal — promenljivu niti
ThreadLocal je alatka u Javi za realizaciju promenljivih lokalnih za nit. Dozvoljava svakoj niti da ima svoj nezavisan primerak, čime se postiže izolacija niti, što se koristi za rešavanje problema nitne bezbednosti deljenih objekata u višenitnom okruženju.
Obično koristimo ključnu reč synchronized ili lock da bismo kontrolisali redosled niti pri pristupu resursima u kritičnoj sekciji, ali ovakav način zaključavanja blokira niti koje nisu pribavile bravu — očigledno, vremenska efikasnost ovog pristupa nije naročito visoka.
Suština problema nitne bezbednosti je u tome što više niti pristupa istom deljenom resursu u kritičnoj sekciji. Ako bi svaka nit imala „sopstveni deljeni resurs“, koristila svoj bez međusobnog uticaja, ne bi ni bilo problema nitne bezbednosti, zar ne?
U stvari, ovo je ideja „prostor za vreme“: svaka nit ima svoj sopstveni resurs. Iako memorija raste, pošto nema sinhronizacije, smanjuju se mogući problemi blokiranja niti, čime se poboljšava vremenska efikasnost.
Ipak, ThreadLocal se ne nalazi u paketu java.util.concurrent već u paketu java.lang, ali ga ja radije posmatram kao vrstu konkurentnog kontejnera.
Kao što ime kaže, ThreadLocal je „lokalna promenljiva“ niti: svaka nit poseduje po jedan primerak te promenljive, tako da svako ima svoj primerak, čime se izbegava takmičenje za deljeni resurs.
Analiza izvornog koda ThreadLocal-a
Metod set
Metod set postavlja vrednost ThreadLocal promenljive unutar trenutne niti. Izvorni kod metoda je:
public void set(T value) {
//1. Dobavlja instancu trenutne niti
Thread t = Thread.currentThread();
//2. Preko instance trenutne niti dobavlja objekat ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//3. Ako Map nije null, sa trenutnom instancom ThreadLocal kao ključem, upisuje par (this, value)
map.set(this, value);
else
//4. Ako je map null, kreira novi ThreadLocalMap i upisuje value
createMap(t, value);
}- Metodom
Thread.currentThread()dobavlja se instanca niti koja poziva ovaj metod. - Svaka nit ima svoj ThreadLocalMap; ta mapa čuva lokalne promenljive niti, pri čemu je ključ ThreadLocal objekat, a vrednost objekat specifičan za nit.
- Ako Map nije null, sa trenutnom instancom ThreadLocal kao ključem i value kao vrednošću upisuje se par; ako je map null, kreira se novi ThreadLocalMap i upisuje se value.
Iz izvornog koda vidimo da se value čuva u ThreadLocalMap. Pogledajmo šta je ThreadLocalMap — najpre ćemo steci osnovni utisak, a kasnije ćemo ga detaljno razmotriti.
Odakle potiče ThreadLocalMap? Iz getMap(t):
ThreadLocalMap getMap(Thread t) {
return t.ThreadLocals;
}Ovaj metod direktno vraća člansku promenljivu ThreadLocals objekta niti t:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap ThreadLocals = null;Pogledajmo sada metod set — kada je map null, kreira se novi preko createMap(t, value):
void createMap(Thread t, T firstValue) {
t.ThreadLocals = new ThreadLocalMap(this, firstValue);
}Ovaj metod kreira novu instancu ThreadLocalMap, sa trenutnom instancom ThreadLocal kao ključem i value kao vrednošću je smešta u ThreadLocalMap, a zatim promenljivoj ThreadLocals trenutne niti dodeljuje taj ThreadLocalMap objekat.
Značaj metoda set je u tome što osigurava da svaka nit ima sopstveni primerak promenljive. Pošto se te promenljive čuvaju u mapi povezanoj sa niti, međusobno se ne utiču između različitih niti.
Metod get
Metod get dobavlja vrednost ThreadLocal promenljive unutar trenutne niti. Pogledajmo izvorni kod:
public T get() {
//1. Dobavlja instancu trenutne niti
Thread t = Thread.currentThread();
//2. Dobavlja ThreadLocalMap trenutne niti
ThreadLocalMap map = getMap(t);
if (map != null) {
//3. Dobavlja entry iz mape čiji je ključ instanca ThreadLocal
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4. Ako entry nije null, vraća odgovarajuću vrednost
T result = (T)e.value;
return result;
}
}
//5. Ako je map null ili je entry null, inicijalizuje se preko ovog metoda i vraća njegova vrednost
return setInitialValue();
}Logiku koda pratite po komentarima; pogledajmo šta radi setInitialValue:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}Logika ovog metoda gotovo je ista kao kod set-a; pogledajmo pre svega metod initialValue:
protected T initialValue() {
return null;
}Ovaj metod je protected, što znači da potklase ThreadLocal-a mogu da ga prekriju i zadole odgovarajuću početnu vrednost.
Evo tipične upotrebe initialValue:
private static ThreadLocal<Integer> myThreadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0; // početna vrednost postavljena na 0
}
};Ovaj isečak kreira novi ThreadLocal<Integer> objekat sa početnom vrednošću 0. Bilo koja nit koja prvi put pokuša da pristupi ovoj ThreadLocal promenljivoj videće vrednost 0.
Svrha celog metoda setInitialValue je da osigura da svaka nit pri prvom pokušaju pristupa svojoj ThreadLocal promenljivoj ima odgovarajuću vrednost. Ovakav „lenji“ pristup inicijalizaciji osigurava da se vrednosti specifične za nit kreiraju tek kada su zaista potrebne.
Metod remove
public void remove() {
//1. Dobavlja ThreadLocalMap trenutne niti
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//2. Iz mape uklanja par čiji je ključ instanca ThreadLocal
m.remove(this);
}Metod remove uklanja iz ThreadLocalMap-a trenutne niti onaj unos koji je povezan sa trenutnom instancom ThreadLocal. Ovaj metod je posebno koristan pri oslobađanju resursa nitne lokalne promenljive ili pri resetovanju njene vrednosti.
Primer koda koji koristi metod remove:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Initial Value");
Thread thread = new Thread(() -> {
System.out.println(threadLocal.get()); // ispisuje "Initial Value"
threadLocal.set("Updated Value");
System.out.println(threadLocal.get()); // ispisuje "Updated Value"
threadLocal.remove();
System.out.println(threadLocal.get()); // ispisuje "Initial Value"
});
thread.start();Izlaz:
Initial Value
Updated Value
Initial ValueAnaliza izvornog koda ThreadLocalMap-a
ThreadLocalMap je statička unutrašnja klasa klase ThreadLocal; to je prilagođena hes-tabela namenjena čuvanju lokalnih promenljivih svake niti.
static class ThreadLocalMap {}Kao i većina kontejnera, ThreadLocalMap interno održava niz tipa Entry, nazvan table, čija je dužina stepen broja dva.
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;Pogledajmo šta je Entry:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}Entry nasleđuje slabu referencu WeakReference<ThreadLocal<?>>; njegovo polje value služi za čuvanje vrednosti povezane sa određenim ThreadLocal objektom. Korišćenje slabe reference kao ključa dozvoljava garbage collector-u da, kad ThreadLocal instanca više nije potrebna, istu i povrati.
Ovde možemo jednom ilustracijom razumeti odnos između Thread, ThreadLocal, ThreadLocalMap i Entry:

Pune linije na slici označavaju jake reference, isprekidane slabe reference. Svaka nit preko ThreadLocals može doći do ThreadLocalMap-a, a ThreadLocalMap je zapravo niz Entry čiji je ključ instanca ThreadLocal, a vrednost proizvoljan objekat.
Kada ThreadLocal promenljivoj dodeljujemo vrednost, zapravo u taj ThreadLocalMap smeštamo Entry sa trenutnom instancom ThreadLocal kao ključem i vrednošću.
Obratite pažnju: ključ Entry-ja je slaba referenca, što znači da kada spoljna jaka referenca ThreadLocal-a postane null (ThreadLocalInstance=null), prema analizi dostižnosti, instanca ThreadLocal tada više nema nijednu lančanu referencu koja je vezuje, pa će je GC pri sakupljanju povratiti.
Tako ThreadLocalMap dobija Entry čiji je ključ null, pa se vrednosti tih ključeva više ne mogu pristupiti; ako se nit dugo ne završava, te vrednosti sa ključem null ostaju na jakoj referentnoj vezi: Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value, koja se ne može povratiti, što dovodi do curenja memorije.
Naravno, ako se nit završi, ThreadLocal, ThreadLocalMap i Entry nemaju dostižnu referentnu lančanicu pa će ih GC prikupiti. Ali u praksi se niti ne završavaju same, jer se ponovo koriste — na primer u bazama veza; previše veliki bazeni niti mogu povećati rizik od curenja memorije, pa razumno podešavanje veličine bazena niti i vremena života niti pomaže u ublažavanju tog problema.
Da bismo izbegli ovaj problem, najbolje je nakon svake upotrebe ThreadLocal-a izričito pozvati njegov metod remove, koji uklanja vrednost povezanu sa trenutnom niti. Tako se osigurava da nit pri ponovnoj upotrebi ne čuva stare, nepotrebne vrednosti.
Kao i kontejneri ConcurrentHashMap i HashMap, ThreadLocalMap je realizovan preko hes-tabele.
Hes-tabela
Hes-tabela se temelji na nizu; svaki element niza naziva se „kofa“ (Bucket), a u njoj se čuva par (ključ-vrednost). Ključ se generiše hes-funkcijom — idealna hes-funkcija ravnomerno raspoređuje ključeve, čime se sukobi svode na najmanju meru.

Idealna hes-funkcija ravnomerno raspoređuje ključeve, čime se sukobi svode na najmanju meru. Kada su hes-vrednosti dva ili više ključeva iste (tj. mapiraju se u istu kofu), govorimo o hes-sukobu. Uobičajene strategije rešavanja su metoda povezanih listi i metoda otvorenog adresiranja.
Metoda povezanih listi
Pri obradi HashMap detaljno smo obradili metodu povezanih listi, pa se nadamo da vam je ostala u sećanju; ovde ćemo je ukratko podsetiti: kada se neki ključ hesiranjem smesti na određeno mesto u hes-tabeli, podatak se dodaje u povezanu listu; drugi podaci koji se takođe mapiraju na isto mesto samo se dodaju u istu listu. Ispod je ilustracija:

Metoda otvorenog adresiranja
U metodi otvorenog adresiranja, ako podatak ne može neposredno da se smesti pod indeksom koji je izračunala hes-funkcija, traži se druga lokacija. Postoje tri načina traženja: „linearno ispitivanje“, „kvadratno ispitivanje“ i „ponovno hesiranje“.
- Linearno ispitivanje: kada je indeks koji je izračunala hes-funkcija već zauzet, traži se unapred redom dok se ne pronađe slobodno mesto.
Na primer, ako broj 88 nakon hes-funkcije daje indeks 16, ali je na mestu 16 već element, traži se 17; ako je i 17 zauzet, traži se 18, i tako redom dok se ne pronađe prazno mesto. Pogledajte sliku:

U hes-tabelu dodajemo element „Qian Duoduo“; nakon hes-funkcije on dobija indeks 0, ali na mestu 0 je već Zhang San, pa se indeks pomera napred dok na indeksu 4 ne bude prazno — tu se i smešta.
- Kvadratno ispitivanje: kada je indeks iz hes-funkcije zauzet, traži se unapred po određenom obrascu, a ne redom; na primer, koraci pretrage su 1, 2, 4, 8, 16…
U hes-tabeli s linearnim ispitivanjem javlja se grupisanje podataka; jednom formirano, grupa samo raste, a svi podaci čiji hes pada u taj raspon moraju se pomakati korak po korak i umetnuti na kraj grupe, pa što je grupa veća — brže raste. To je kao u supermarketu: kad na jednom mestu bude mnogo ljudi, ljudi sve više pristižu — svi žele samo da znaju šta se tamo dešava.
Kvadratno ispitivanje je pokušaj da se izbegne grupisanje: ideja je ispitivati ćelije koje su udaljenije, a ne neposredno susedne originalnom položaju. Kod linearnog ispitivanja, ako je originalni indeks hes-funkcije x, linearno ispitivanje ispituje x+1, x+2, x+3..., dok kod kvadratnog ispitivanja redosled glasi x+1, x+4, x+9, x+16, x+25... — dakle kvadrati udaljenosti od originala. Radi lakšeg razumevanja pogledajte sliku.

Kod linearnog ispitivanja do mesta Qian Duoduo stižemo u 4 koraka. Kod kvadratnog ispitivanja svaki korak je kvadrat originalne udaljenosti, pa su nam dovoljna samo dva koraka.
- Ponovno hesiranje: kada je indeks iz hes-funkcije zauzet, koristi se druga hes-funkcija za izračunavanje indeksa.
Kvadratno ispitivanje otklanja grupisanje linearnog ispitivanja (koje se naziva primarno grupisanje); međutim, kvadratno ispitivanje uvodi novo grupisanje, jer svi ključevi koji se mapiraju na isto mesto u potrazi za slobodnim prostorom ispituju ista mesta.
Na primer, ako se u hes-tabelu redom ubace 1, 11, 21, 31, 41 — svi se mapiraju na mesto 1; tada 11 ispituje sa korakom jedan, 21 sa korakom četiri, 31 sa korakom devet, 41 sa korakom šesnaest. Čim se jedan element mapira na 1, potreban je duži korak ispitivanja — to je sekundarno grupisanje.
Ponovno hesiranje služi da otkloni i primarno i sekundarno grupisanje, jer i kod linearnog i kod kvadratnog ispitivanja korak ispitivanja uvek ostaje fiksan. Dvostruko hesiranje, pored prve hes-funkcije, dodaje još jednu koja na osnovu ključa generiše korak ispitivanja — tako čak i ako prva hes-funkcija mapira na isti indeks, koraci ispitivanja se razlikuju, što rešava problem grupisanja.
Druga hes-funkcija mora imati sledeće osobine:
- razlikuje se od prve hes-funkcije;
- ne sme davati izlaz 0, jer korak 0 znači da svako ispitivanje cilja isto mesto, što vodi u beskonačnu petlju; eksperimenti pokazuju da hes-funkcija oblika
stepSize = constant-(key%constant);daje odlične rezultate, gde je constant prost broj manji od kapaciteta niza.
Ilustracija:

ThreadLocalMap za rešavanje hes-sukoba koristi metodu otvorenog adresiranja, za razliku od HashMap-a. Razlog za drugačiji pristup je pre svega taj što su hes-vrednosti u ThreadLocalMap-u prilično ravnomerno raspoređene, pa sukoba gotovo da i nema; a pošto ThreadLocalMap često mora da uklanja neupotrebljene objekte, verovatnoća sukoba je još manja.
Metod set
Dakle, nakon upoznavanja sa hes-tabelom, ponovo ćemo pogledati metod set. Izvorni kod metoda set je:
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
//Preko hashCode-a ThreadLocal-a određuje mesto na koje Entry treba da se smesti
int i = key.ThreadLocalHashCode & (len-1);
//Koristi metodu otvorenog adresiranja; pri hes-sukobu linearno ispitivanje
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//Prepisuje stari Entry
if (k == key) {
e.value = value;
return;
}
//Kada je ključ null, to znači da je jaka referenca ThreadLocal-a već oslobođena,
//pa se preko tog ključa više ne može doći do odgovarajućeg entry-ja u ThreadLocalMap-u — ovde postoji mogućnost curenja memorije
if (k == null) {
//Menja taj „prljavi“ entry sa ključem null trenutno umetnutom vrednošću
replaceStaleEntry(key, value, i);
return;
}
}
//Kreira novi entry i umeće ga u table na poziciju i
tab[i] = new Entry(key, value);
int sz = ++size;
//Nakon umetanja ponovo uklanja nekoliko „prljavih“ entry-ja sa ključem null; ako je broj veći od praga, radi proširenje
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}Ključni delovi metoda set pratite po komentarima; ovde treba obratiti pažnju na nekoliko tačaka:
01. hashcode ThreadLocal-a
private final int ThreadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode =new AtomicInteger();
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}hashCode ThreadLocal-a dobavlja se metodom nextHashCode(), koja je zapravo realizovana preko AtomicInteger-a dodavanjem 0x61c88647.
0x61c88647 je magični broj koji služi za uvećavanje hes-koda ThreadLocal-a. Njegov izbor nije slučajan: to je prost broj sa sledećim osobinama:
- Prost broj: ne može se podeliti ni sa čim osim jedinicom i samim sobom.
- Zlatni presek: ovaj broj je približno polovina 32-bitnog pokretnozarezog prikaza zlatnog preseka. Zlatni presek ima zanimljive matematičke osobine, od kojih je jedna veza sa Fibonačijevim nizom.
- Ravnomerno uvećanje: u ThreadLocal-u ovaj broj služi da se hes-kodovi različitih niti rasporede po hes-tabeli, čime se smanjuju sukobi. Pri svakom stvaranju novog ThreadLocal objekta, ova vrednost se dodaje na hes-kod prethodnog ThreadLocal-a. Taj uvećani korak pomaže ravnomernom raspoređivanju ThreadLocal objekata po hes-tabeli.
- Optimizacija performansi: korišćenjem ove konkretne vrednosti algoritam osigurava ravnomerno raspoređivanje hes-kodova, čime se smanjuje verovatnoća hes-sukoba. To je ključno za performanse hes-tabele, jer sukobi smanjuju efikasnost pretrage.
02. Kako se određuje mesto umetanja nove vrednosti?
Preko linije: key.ThreadLocalHashCode & (len-1).
Kao i kod HashMap, preko AND operacije između hes-koda trenutnog ključa i veličine hes-tabele. Princip smo već obradili uz HashMap — ko se ne seća, može se vratiti.
03. Kako se rešava hes-sukob?
Preko nextIndex(i, len), čiji izraz ((i + 1 < len) ? i + 1 : 0); omogućava neprekidno linearno ispitivanje unapred, a po dostizanu kraja hes-tabele kreće ponovo od 0 — formirajući prsten.
04. Kako se rešavaju „prljavi“ Entry-ji?
Znamo da upotreba ThreadLocal-a može dovesti do curenja memorije; za takve Entry-je sa ključem null kažemo da su „stale entry“, što bismo preveli kao „neproveži entry“, a ja ih nazivam „prljavim entry-ima“.
Naravno, Josh Bloch i Doug Lea su o tome već razmislili; izvorni kod nudi sledeća rešenja:
Pri dodavanju novog unosa u ThreadLocalMap može se proveriti postojanje „prljavih“ Entry-ja (čiji je ključ null) i zameniti ih novim. To je upravo ono što radi metod replaceStaleEntry u izvornom kodu.

Tokom pojedinih operacija (npr. dodavanje, dobavljanje) mogu se dodati dodatne čišćenja koja skeniraju i uklanjaju „prljave“ Entry-je. To se postiže obilaskom hes-tabele i brisanjem onih unosa čiji je ključ null. Metod cleanSomeSlots u izvornom kodu je primer takvog čišćenja.

05. Kako se radi proširenje?
Kao i HashMap, i ThreadLocalMap ima mehanizam proširenja. Kako se određuje njegov threshold?
private int threshold; // Default to 0
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.ThreadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}Pri prvom dodeljivanju vrednosti ThreadLocal-u kreira se ThreadLocalMap početne veličine 16, a metodom setThreshold postavlja se threshold na dve trećine dužine hes-niza — dakle faktor opterećenja iznosi 2/3.
Faktor opterećenja (Load Factor) važan je pojam hes-tabele; on predstavlja odnos broja unosa u hes-tabeli prema njenom kapacitetu. Faktor opterećenja služi kao mera popunjenosti hes-tabele i utiče na performanse operacija pretrage, umetanja i brisanja. Sećamo se da je faktor opterećenja HashMap-a 0,75.
Ovde je početna veličina ThreadLocalMap-a 16, faktor opterećenja 2/3, pa je raspoloživa veličina hes-tabele: 16*2/3=10, dakle raspoloživi kapacitet je 10.
Kada size hes-tabele premaši threshold, proširenje se vrši metodom resize.
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//Novi niz je dvostruko veći od originalnog
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
//Ako pri obilasku naiđe na prljavi entry, postavlja value na null, što olakšava GC-u da ga povrati
if (k == null) {
e.value = null; // Help the GC
} else {
//Ponovo određuje poziciju entry-ja u novom nizu i umeće ga
int h = k.ThreadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//Postavlja threshold i size novog hes-niza
setThreshold(newLen);
size = count;
table = newTab;
}Logiku metoda pratite po komentarima: novi niz je dvostruko duži od originalnog, a zatim se obilaze entry-i starog niza i umeću u novi. Obratite pažnju da je ovaj kod vrlo temeljito napisan — pri proširenju se za prljave entry-je value postavlja na null kako bi ih GC mogao povratiti, čime se rešava skriveno curenje memorije.
Metod getEntry
Izvorni kod metoda getEntry je:
private Entry getEntry(ThreadLocal<?> key) {
//1. Određuje poziciju u hes-nizu
int i = key.ThreadLocalHashCode & (table.length - 1);
//2. Dobavlja entry po indeksu i
Entry e = table[i];
//3. Ako uslovi vrede, vraća taj entry
if (e != null && e.get() == key)
return e;
else
//4. Ako ne pronađe entry koji zadovoljava, radi dodatnu obradu
return getEntryAfterMiss(key, i, e);
}Logika metoda je jednostavna: ako je ključ trenutnog entry-ja jednak traženom ključu, taj entry se vraća; inače se preko getEntryAfterMiss radi dodatna obrada: ako je unos na indeksu null ili mu ključ ne odgovara zadatom, potrebno je pozvati getEntryAfterMiss radi obrade mogućeg hes-sukoba.
Metod getEntryAfterMiss je:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
//Ako pronađe entry čiji ključ odgovara traženom, vraća ga
return e;
if (k == null)
//Rešava problem prljavih entry-ja
expungeStaleEntry(i);
else
//Nastavlja kružno pretraživanje unapred
i = nextIndex(i, len);
e = tab[i];
}
return null;
}Metod getEntryAfterMiss služi za traženje unosa u ThreadLocalMap-u u slučaju hes-sukoba: strategijom otvorenog adresiranja pretražuje ostale pozicije u hes-tabeli i prikladno obrađuje „prljave“ unose.
Metod remove
Posmatrajmo direktno izvorni kod:
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.ThreadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//Postavlja ključ entry-ja na null
e.clear();
//I value entry-ja postavlja na null
expungeStaleEntry(i);
return;
}
}
}Preko lokalne promenljive tab dobavlja hes-niz ThreadLocalMap-a; len je njegova dužina.
Izrazom
key.ThreadLocalHashCode & (len-1)računa se hes-indeks za zadati ThreadLocal ključ. To određuje od kog indeksa počinje pretraga.Strategijom otvorenog adresiranja obilazi hes-tabelu; izrazom
nextIndex(i, len)računa se sledeći indeks radi obrade hes-sukoba.Ako pronađe unos čiji ključ odgovara zadatom (tj.
e.get() == key), izvršava sledeće:
- Brisanje ključa: pozivom
e.clear()ključ unosa se postavlja na null. Pošto je Entry potklasa WeakReference, metod clear prekida referencu na ThreadLocal objekat, dozvoljavajući GC-u da ga po potrebi povrati. - Brisanje vrednosti: pozivom
expungeStaleEntry(i)briše se vrednost unosa i radi se delimično čišćenje hes-tabele. Cilj ovog metoda je da ukloni nevažeće unose, odnosno one čiji je ključ već prikupljen od strane GC-a.
- Završetak operacije brisanja: jednom pronađen i obrisan odgovarajući unos, metod se vraća. Ako se obilaskom cele hes-tabele ne pronađe odgovarajući ključ, metod ne radi ništa i normalno se završava.
Scenariji upotrebe ThreadLocal-a
Scenariji upotrebe ThreadLocal-a su brojni, na primer:
- za čuvanje podataka o prijavi korisnika, tako da bilo gde u istoj niti mogu dohvatiti podatke o prijavi;
- za čuvanje veze sa bazom, Session objekata i sl., tako da bilo gde u istoj niti mogu dohvatiti vezu sa bazom, Session objekat;
- za čuvanje konteksta transakcije, tako da bilo gde u istoj niti mogu dohvatiti kontekst transakcije;
- za čuvanje promenljivih unutar niti, tako da bilo gde u istoj niti mogu dohvatiti promenljive te niti.
Ispod je primer koji koristi ThreadLocal za čuvanje podataka o prijavi korisnika. Ovaj primer je pogodan za višenitna okruženja poput web servera, gde svaka nit opslužuje jedan nezavisan zahtev korisnika.
public class UserAuthenticationService {
// Kreira instancu ThreadLocal za čuvanje podataka o prijavi korisnika
private static ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);
public static void main(String[] args) {
// Simulira prijavu korisnika
loginUser(new User("Alice", "password123"));
System.out.println("User logged in: " + getCurrentUser().getUsername());
// Simulira drugu nit koja opslužuje drugog korisnika
Runnable task = () -> {
loginUser(new User("Bob", "password456"));
System.out.println("User logged in: " + getCurrentUser().getUsername());
};
Thread thread = new Thread(task);
thread.start();
}
// Simulira metod prijave korisnika
public static void loginUser(User user) {
// Ovde bi obično bio neki logika provere identiteta
currentUser.set(user);
}
// Dobavlja podatke o korisniku povezane sa trenutnom niti
public static User getCurrentUser() {
return currentUser.get();
}
// Korisnička klasa
public static class User {
private final String username;
private final String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
// ostali getter-i i setter-i...
}
}Ovaj primer definiše klasu UserAuthenticationService koja koristi ThreadLocal za čuvanje podataka o prijavi korisnika povezanih sa trenutnom niti. Pod pretpostavkom da je korisnik već prošao proveru identiteta, objekat korisnika se čuva u ThreadLocal promenljivoj currentUser. Metod getCurrentUser služi za dohvatanje podataka o korisniku povezanih sa trenutnom niti. Pošto se koristi ThreadLocal, različite niti mogu istovremeno prijaviti različite korisnike, bez međusobnog ometanja.
Kratak pregled
ThreadLocal je vrlo korisna alatka koja može poslužiti za čuvanje promenljivih unutar niti, tako da bilo gde u istoj niti mogu biti dohvaćene. Ipak, ThreadLocal se lako može zloupotrebiti: ako se ne koristi pažljivo, može dovesti do curenja memorije.
ThreadLocalMap je jezgro ThreadLocal-a — hes-tabela čiji je ključ instanca ThreadLocal, a vrednost proizvoljan objekat. ThreadLocalMap hes-sukobe rešava metodom otvorenog adresiranja; njegov početni kapacitet je 16, faktor opterećenja 2/3, a pri proširenju se kapacitet udvostručuje.
Urednik: Chenmo Wang Er, deo sadržaja potiče iz GitHub repozitorijuma CL0610 https://github.com/CL0610/Java-concurrency, a deo slika i sadržaja iz ovog posta na Zhihu-u.
Na GitHub-u je konačno stigao drugi PDF „Mali priručnik o konkurentnom programiranju“open-source baze znanja sa preko 17000 zvezdica „Ergov put ka naprednom Javom“! Obuhvata osnovne pojmove i načine upotrebe niti, memorijski model Java-e, synchronized, volatile, CAS, AQS, ReentrantLock, bazene niti, konkurentne kontejnere, ThreadLocal, model proizvođač-potrošač i druge teme koje su obavezne za intervjue i razvoj — ukupno preko 150 000 reči i preko 200 ručno nacrtanih ilustracija, što se može opisati kao pristupačno i sa humorom... Više detalja: Odlično, Ergov put ka naprednom konkurentnom programiranju.pdf
