av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

ThreadLocal 你真的用不上嗎?

   

  • ThreadLocal的作用以及應(yīng)用場(chǎng)景
  •  使用場(chǎng)景
  •  原理分析
  •  ThreadLocalMap的底層結(jié)構(gòu)
  •  內(nèi)存泄露產(chǎn)生的原因
  •  解決Hash沖突
  •  使用ThreadLocal時(shí)對(duì)象存在哪里?

ThreadLocal的作用以及應(yīng)用場(chǎng)景

ThreadLocal算是一種并發(fā)容器吧,因?yàn)樗膬?nèi)部是有ThreadLocalMap組成,ThreadLocal是為了解決多線程情況下變量不能被共享的問(wèn)題,也就是多線程共享變量的問(wèn)題。

ThreadLocal和Lock以及Synchronized的區(qū)別是:ThreadLocal是給每個(gè)線程分配一個(gè)變量(對(duì)象),各個(gè)線程都存有變量的副本,這樣每個(gè)線程都是使用自己(變量)對(duì)象實(shí)例,使線程與線程之間進(jìn)行隔離;而Lock和Synchronized的方式是使線程有順序的執(zhí)行。

舉一個(gè)簡(jiǎn)單的例子:目前有100個(gè)學(xué)生等待簽字,但是老師只有一個(gè)筆,那老師只能按順序的分給每個(gè)學(xué)生,等待A學(xué)生簽字完成然后將筆交給B學(xué)生,這就類似Lock,Synchronized的方式。而ThreadLocal是,老師直接拿出一百個(gè)筆給每個(gè)學(xué)生;再效率提高的同事也要付出一個(gè)內(nèi)存消耗;也就是以空間換時(shí)間的概念

使用場(chǎng)景

Spring的事務(wù)隔離就是使用ThreadLocal和AOP來(lái)解決的;主要是TransactionSynchronizationManager這個(gè)類;

解決SimpleDateFormat線程不安全問(wèn)題;

當(dāng)我們使用SimpleDateFormat的parse()方法的時(shí)候,parse()方法會(huì)先調(diào)用Calendar.clear()方法,然后調(diào)用Calendar.add()方法,如果一個(gè)線程先調(diào)用了add()方法,然后另一個(gè)線程調(diào)用了clear()方法;這時(shí)候parse()方法就會(huì)出現(xiàn)解析錯(cuò)誤;如果不信我們可以來(lái)個(gè)例子:

public class SimpleDateFormatTest {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dateFormat();
}
});
thread.start();
}
}
/**
* 字符串轉(zhuǎn)成日期類型
*/
public static void dateFormat() {
try {
simpleDateFormat.parse("2021-5-27");
} catch (ParseException e) {
e.printStackTrace();
}
}
}

這里我們只啟動(dòng)了50個(gè)線程問(wèn)題就會(huì)出現(xiàn),其實(shí)看巧不巧,有時(shí)候只有10個(gè)線程的情況就會(huì)出錯(cuò):

Exception in thread "Thread-40" java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at cn.haoxy.use.lock.sdf.SimpleDateFormatTest.dateFormat(SimpleDateFormatTest.java:36)
at cn.haoxy.use.lock.sdf.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:23)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-43" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at .............

其實(shí)解決這個(gè)問(wèn)題很簡(jiǎn)單,讓每個(gè)線程new一個(gè)自己的SimpleDateFormat,但是如果100個(gè)線程都要new100個(gè)SimpleDateFormat嗎?

當(dāng)然我們不能這么做,我們可以借助線程池加上ThreadLocal來(lái)解決這個(gè)問(wèn)題:

public class SimpleDateFormatTest {
private static ThreadLocal local = new ThreadLocal() {
@Override
//初始化線程本地變量
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
for (int i = 0; i < 500; i++) {
es.execute(() -> {
//調(diào)用字符串轉(zhuǎn)成日期方法
dateFormat();
});
}
es.shutdown();
}
/**
* 字符串轉(zhuǎn)成日期類型
*/
public static void dateFormat() {
try {
//ThreadLocal中的get()方法
local.get().parse("2021-5-27");
} catch (ParseException e) {
e.printStackTrace();
}
}
}

這樣就優(yōu)雅的解決了線程安全問(wèn)題;

解決過(guò)度傳參問(wèn)題;例如一個(gè)方法中要調(diào)用好多個(gè)方法,每個(gè)方法都需要傳遞參數(shù);例如下面示例:

void work(User user) {
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}

用了ThreadLocal之后:

public class ThreadLocalStu {
private static ThreadLocal userThreadLocal = new ThreadLocal<>();
void work(User user) {
try {
userThreadLocal.set(user);
getInfo();
checkInfo();
someThing();
} finally {
userThreadLocal.remove();
}
}
void setInfo() {
User u = userThreadLocal.get();
//.....
}
void checkInfo() {
User u = userThreadLocal.get();
//....
}
void someThing() {
User u = userThreadLocal.get();
//....
}
}

每個(gè)線程內(nèi)需要保存全局變量(比如在登錄成功后將用戶信息存到ThreadLocal里,然后當(dāng)前線程操作的業(yè)務(wù)邏輯直接get取就完事了,有效的避免的參數(shù)來(lái)回傳遞的麻煩之處),一定層級(jí)上減少代碼耦合度。

  •  比如存儲(chǔ) 交易id等信息。每個(gè)線程私有。
  •  比如aop里記錄日志需要before記錄請(qǐng)求id,end拿出請(qǐng)求id,這也可以。
  •  比如jdbc連接池(很典型的一個(gè)ThreadLocal用法)
  •  ....等等....

原理分析

上面我們基本上知道了ThreadLocal的使用方式以及應(yīng)用場(chǎng)景,當(dāng)然應(yīng)用場(chǎng)景不止這些這只是工作中常用到的場(chǎng)景;下面我們對(duì)它的原理進(jìn)行分析;

我們先看一下它的set()方法;

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

是不是特別簡(jiǎn)單,首先獲取當(dāng)前線程,用當(dāng)前線程作為key,去獲取ThreadLocalMap,然后判斷map是否為空,不為空就將當(dāng)前線程作為key,傳入的value作為map的value值;如果為空就創(chuàng)建一個(gè)ThreadLocalMap,然后將key和value方進(jìn)去;從這里可以看出value值是存放到ThreadLocalMap中;

然后我們看看ThreadLocalMap是怎么來(lái)的?先看下getMap()方法:

//在Thread類中維護(hù)了threadLocals變量,注意是Thread類
ThreadLocal.ThreadLocalMap threadLocals = null;
//在ThreadLocal類中的getMap()方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

這就能解釋每個(gè)線程中都有一個(gè)ThreadLocalMap,因?yàn)門(mén)hreadLocalMap的引用在Thread中維護(hù);這就確保了線程間的隔離;

我們繼續(xù)回到set()方法,看到當(dāng)map等于空的時(shí)候createMap(t, value);

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

這里就是new了一個(gè)ThreadLocalMap然后賦值給threadLocals成員變量;ThreadLocalMap構(gòu)造方法:

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
//初始化一個(gè)Entry
table = new Entry[INITIAL_CAPACITY];
//計(jì)算key應(yīng)該存放的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//將Entry放到指定位置
table[i] = new Entry(firstKey, firstValue);
size = 1;
//設(shè)置數(shù)組的大小 16*2/3=10,類似HashMap中的0.75*16=12
setThreshold(INITIAL_CAPACITY);
}

這里寫(xiě)有個(gè)大概的印象,后面對(duì)ThreadLocalMap內(nèi)部結(jié)構(gòu)還會(huì)進(jìn)行詳細(xì)的講解;

下面我們?cè)偃タ匆幌耮et()方法:

public T get() {
Thread t = Thread.currentThread();
//用當(dāng)前線程作為key去獲取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//map不為空,然后獲取map中的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//如果Entry不為空就獲取對(duì)應(yīng)的value值
T result = (T)e.value;
return result;
}
}
//如果map為空或者entry為空的話通過(guò)該方法初始化,并返回該方法的value
return setInitialValue();
}

get()方法和set()都比較容易理解,如果map等于空的時(shí)候或者entry等于空的時(shí)候我們看看setInitialValue()方法做了什么事:

private T setInitialValue() {
//初始化變量值 由子類去實(shí)現(xiàn)并初始化變量
T value = initialValue();
Thread t = Thread.currentThread();
//這里再次getMap();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//和set()方法中的
createMap(t, value);
return value;
}

下面我們?cè)偃タ匆幌耇hreadLocal中的initialValue()方法:

protected T initialValue() {
return null;
}

設(shè)置初始值,由子類去實(shí)現(xiàn);就例如我們上面的例子,重寫(xiě)ThreadLocal類中的initialValue()方法:

private static ThreadLocal local = new ThreadLocal() {
@Override
//初始化線程本地變量
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};

createMap()方法和上面set()方法中createMap()方法同一個(gè),就不過(guò)多的敘述了;剩下還有一個(gè)removve()方法。

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//2. 從map中刪除以當(dāng)前threadLocal實(shí)例為key的鍵值對(duì)
m.remove(this);
}

源碼的講解就到這里,也都比較好理解,下面我們看看ThreadLocalMap的底層結(jié)構(gòu)。

ThreadLocalMap的底層結(jié)構(gòu)

上面我們已經(jīng)了解了ThreadLocal的使用場(chǎng)景以及它比較重要的幾個(gè)方法;下面我們?cè)偃ニ膬?nèi)部結(jié)構(gòu);經(jīng)過(guò)上的源碼分析我們可以看到數(shù)據(jù)其實(shí)都是存放到了ThreadLocal中的內(nèi)部類ThreadLocalMap中;而ThreadLocalMap中又維護(hù)了一個(gè)Entry對(duì)象,也就說(shuō)數(shù)據(jù)最終是存放到Entry對(duì)象中的;

static class ThreadLocalMap {
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
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);
}
// ....................
}

Entry的構(gòu)造方法是以當(dāng)前線程為key,變量值Object為value進(jìn)行存儲(chǔ)的;在上面的源碼中ThreadLocalMap的構(gòu)造方法中也涉及到了Entry;看到Entry是一個(gè)數(shù)組;初始化長(zhǎng)度為INITIAL_CAPACITY = 16;因?yàn)?Entry 繼承了 WeakReference,在 Entry 的構(gòu)造方法中,調(diào)用了 super(k)方法就會(huì)將 threadLocal 實(shí)例包裝成一個(gè) WeakReferenece。這也是ThreadLocal會(huì)產(chǎn)生內(nèi)存泄露的原因;

內(nèi)存泄露產(chǎn)生的原因

圖片

如圖所示存在一條引用鏈:Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value,經(jīng)過(guò)上面的講解我們知道ThreadLocal作為Key,但是被設(shè)置成了弱引用,弱引用在JVM垃圾回收時(shí)是優(yōu)先回收的,就是說(shuō)無(wú)論內(nèi)存是否足夠弱引用對(duì)象都會(huì)被回收;弱引用的生命周期比較短;當(dāng)發(fā)生一次GC的時(shí)候就會(huì)變成如下:

圖片

TreadLocalMap中出現(xiàn)了Key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果線程遲遲不結(jié)束(也就是說(shuō)這條引用鏈無(wú)意義的一直存在)就會(huì)造成value永遠(yuǎn)無(wú)法回收造成內(nèi)存泄露;如果當(dāng)前線程運(yùn)行結(jié)束Thread,ThreadLocalMap,Entry之間沒(méi)有了引用鏈,在垃圾回收的時(shí)候就會(huì)被回收;但是在開(kāi)發(fā)中我們都是使用線程池的方式,線程池的復(fù)用不會(huì)主動(dòng)結(jié)束;所以還是會(huì)存在內(nèi)存泄露問(wèn)題;

解決方法也很簡(jiǎn)單,就是在使用完之后主動(dòng)調(diào)用remove()方法釋放掉;

解決Hash沖突

記得在大學(xué)學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)的時(shí)候?qū)W習(xí)了很多種解決hash沖突的方法;例如:

線性探測(cè)法(開(kāi)放地址法的一種): 計(jì)算出的散列地址如果已被占用,則按順序找下一個(gè)空位。如果找到末尾還沒(méi)有找到空位置就從頭重新開(kāi)始找;

圖片

二次探測(cè)法(開(kāi)放地址法的一種)

圖片

鏈地址法:鏈地址是對(duì)每一個(gè)同義詞都建一個(gè)單鏈表來(lái)解決沖突,HashMap采用的是這種方法;

圖片

多重Hash法: 在key沖突的情況下多重hash,直到不沖突為止,這種方式不易產(chǎn)生堆積但是計(jì)算量太大;

公共溢出區(qū)法: 這種方式需要兩個(gè)表,一個(gè)存基礎(chǔ)數(shù)據(jù),另一個(gè)存放沖突數(shù)據(jù)稱為溢出表;

上面的圖片都是在網(wǎng)上找到的一些資料,和大學(xué)時(shí)學(xué)習(xí)時(shí)的差不多我就直接拿來(lái)用了;也當(dāng)自己復(fù)習(xí)了一遍;

介紹了那么多解決Hash沖突的方法,那ThreadLocalMap使用的哪一種方法呢?我們可以看一下源碼:

private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根據(jù)HashCode & 數(shù)組長(zhǎng)度 計(jì)算出數(shù)組該存放的位置
int i = key.threadLocalHashCode & (len-1);
//遍歷Entry數(shù)組中的元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
//如果這個(gè)Entry對(duì)象的key正好是即將設(shè)置的key,那么就刷新Entry中的value;
if (k == key) {
e.value = value;
return;
}
// entry!=null,key==null時(shí),說(shuō)明threadLcoal這key已經(jīng)被GC了,這里就是上面說(shuō)到
//會(huì)有內(nèi)存泄露的地方,當(dāng)然作者也知道這種情況的存在,所以這里做了一個(gè)判斷進(jìn)行解決臟的
//entry(數(shù)組中不想存有過(guò)時(shí)的entry),但是也不能解決泄露問(wèn)題,因?yàn)榕fvalue還存在沒(méi)有消失
if (k == null) {
//用當(dāng)前插入的值代替掉這個(gè)key為null的“臟”entry
replaceStaleEntry(key, value, i);
return;
}
}
//新建entry并插入table中i處
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

從這里我們可以看出使用的是線性探測(cè)的方式來(lái)解決hash沖突!

源碼中通過(guò)nextIndex(i, len)方法解決 hash 沖突的問(wèn)題,該方法為((i + 1 < len) ? i + 1 : 0);,也就是不斷往后線性探測(cè),直到找到一個(gè)空的位置,當(dāng)?shù)焦1砟┪驳臅r(shí)候還沒(méi)有找到空位置再?gòu)?0 開(kāi)始找,成環(huán)形!

使用ThreadLocal時(shí)對(duì)象存在哪里?

在java中,棧內(nèi)存歸屬于單個(gè)線程,每個(gè)線程都會(huì)有一個(gè)棧內(nèi)存,其存儲(chǔ)的變量只能在其所屬線程中可見(jiàn),即棧內(nèi)存可以理解成線程的私有變量,而堆內(nèi)存中的變量對(duì)所有線程可見(jiàn),可以被所有線程訪問(wèn)!

那么ThreadLocal的實(shí)例以及它的值是不是存放在棧上呢?其實(shí)不是的,因?yàn)門(mén)hreadLocal的實(shí)例實(shí)際上也是被其創(chuàng)建的類持有,(更頂端應(yīng)該是被線程持有),而ThreadLocal的值其實(shí)也是被線程實(shí)例持有,它們都是位于堆上,只是通過(guò)一些技巧將可見(jiàn)性修改成了線程可見(jiàn)。


本文名稱:ThreadLocal 你真的用不上嗎?
轉(zhuǎn)載來(lái)于:http://uogjgqi.cn/article/dhsopoo.html
掃二維碼與項(xiàng)目經(jīng)理溝通

我們?cè)谖⑿派?4小時(shí)期待你的聲音

解答本文疑問(wèn)/技術(shù)咨詢/運(yùn)營(yíng)咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流