日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
ThreadLocal 你真的用不上嗎?

   

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

ThreadLocal的作用以及應用場景

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

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

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

使用場景

Spring的事務隔離就是使用ThreadLocal和AOP來解決的;主要是TransactionSynchronizationManager這個類;

解決SimpleDateFormat線程不安全問題;

當我們使用SimpleDateFormat的parse()方法的時候,parse()方法會先調(diào)用Calendar.clear()方法,然后調(diào)用Calendar.add()方法,如果一個線程先調(diào)用了add()方法,然后另一個線程調(diào)用了clear()方法;這時候parse()方法就會出現(xiàn)解析錯誤;如果不信我們可以來個例子:

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();
}
}
}

這里我們只啟動了50個線程問題就會出現(xiàn),其實看巧不巧,有時候只有10個線程的情況就會出錯:

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 .............

其實解決這個問題很簡單,讓每個線程new一個自己的SimpleDateFormat,但是如果100個線程都要new100個SimpleDateFormat嗎?

當然我們不能這么做,我們可以借助線程池加上ThreadLocal來解決這個問題:

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)雅的解決了線程安全問題;

解決過度傳參問題;例如一個方法中要調(diào)用好多個方法,每個方法都需要傳遞參數(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();
//....
}
}

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

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

原理分析

上面我們基本上知道了ThreadLocal的使用方式以及應用場景,當然應用場景不止這些這只是工作中常用到的場景;下面我們對它的原理進行分析;

我們先看一下它的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);
}

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

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

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

這就能解釋每個線程中都有一個ThreadLocalMap,因為ThreadLocalMap的引用在Thread中維護;這就確保了線程間的隔離;

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

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

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

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
//初始化一個Entry
table = new Entry[INITIAL_CAPACITY];
//計算key應該存放的位置
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);
}

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

下面我們再去看一下get()方法:

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

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

private T setInitialValue() {
//初始化變量值 由子類去實現(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;
}

下面我們再去看一下ThreadLocal中的initialValue()方法:

protected T initialValue() {
return null;
}

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

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

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

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

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

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

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

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

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

圖片

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

圖片

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

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

解決Hash沖突

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

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

圖片

二次探測法(開放地址法的一種)

圖片

鏈地址法:鏈地址是對每一個同義詞都建一個單鏈表來解決沖突,HashMap采用的是這種方法;

圖片

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

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

上面的圖片都是在網(wǎng)上找到的一些資料,和大學時學習時的差不多我就直接拿來用了;也當自己復習了一遍;

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

private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根據(jù)HashCode & 數(shù)組長度 計算出數(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();
//如果這個Entry對象的key正好是即將設(shè)置的key,那么就刷新Entry中的value;
if (k == key) {
e.value = value;
return;
}
// entry!=null,key==null時,說明threadLcoal這key已經(jīng)被GC了,這里就是上面說到
//會有內(nèi)存泄露的地方,當然作者也知道這種情況的存在,所以這里做了一個判斷進行解決臟的
//entry(數(shù)組中不想存有過時的entry),但是也不能解決泄露問題,因為舊value還存在沒有消失
if (k == null) {
//用當前插入的值代替掉這個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();
}

從這里我們可以看出使用的是線性探測的方式來解決hash沖突!

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

使用ThreadLocal時對象存在哪里?

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

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


當前名稱:ThreadLocal 你真的用不上嗎?
文章起源:http://m.5511xx.com/article/dhsopoo.html