1.ThreadLocal的用途
場(chǎng)景1:
每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象(通常是工具類淑仆,典型需要使用的類有SimpleDateFormat和Random)
看問(wèn)題代碼
打印1000個(gè)不同線程
public class ThreadLocalNormalUsage03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//參數(shù)的單位是毫秒,從1970.1.1 00:00:00 GMT計(jì)時(shí)
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
所用的線程都共用同一個(gè)SimpleDateFormat對(duì)象均驶,發(fā)生了線程安全問(wèn)題救湖。
解決
public class ThreadLocalNormalUsage05 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//參數(shù)的單位是毫秒,從1970.1.1 00:00:00 GMT計(jì)時(shí)
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
/**
* 寫(xiě)法一
*/
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
/**
* Lambda表達(dá)式
*/
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
場(chǎng)景2:
每個(gè)線程內(nèi)需要保存全局變量(例如在攔截器中獲取用戶信息)艾疟,可以讓不同方法直接使用押框,避免參數(shù)傳遞的麻煩
實(shí)例:當(dāng)前用戶信息需要被線程內(nèi)所有方法共享
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process("kpioneer");
new Service2().process();
new Service3().process();
}
}
class Service1 {
public void process(String name) {
User user = new User(name);
UserContextHolder.holder.set(user);
System.out.println("Service1設(shè)置用戶名:" + user.name);
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.get();
System.out.println("Service2拿到用戶名:" + user.name);
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用戶名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
Service1設(shè)置用戶名:kpioneer
Service2拿到用戶名:kpioneer
Service3拿到用戶名:kpioneer
使用TreadLocal,這樣無(wú)需synchronized尿背,可以在不影響性能的情況下端仰,也無(wú)需層層傳遞參數(shù),就可以達(dá)到保存當(dāng)前線程對(duì)應(yīng)的用戶信息的目的田藐。
2. ThreadLocal的兩個(gè)作用
- 讓某個(gè)需要用的對(duì)象在線程間隔離(每個(gè)線程都有自己的獨(dú)立的對(duì)象)
- 在任何方法中都可以輕松獲取到該對(duì)象荔烧。
場(chǎng)景一:initialValue
在ThreadLocal第一次get的時(shí)候把對(duì)象給初始化出來(lái),對(duì)象的初始化時(shí)機(jī)可以由我們控制
場(chǎng)景二:set
如果需要保存到ThreadLocal里的對(duì)象的生成時(shí)機(jī)不由我們隨意控制汽久,例如攔截器生成的用戶信息茴晋,用ThreadLocal.set直接放到我們的ThreadLocal中去,以便后續(xù)使用回窘。
3.主要方法介紹
initialValue()
- 該方法會(huì)返回當(dāng)前線程對(duì)應(yīng)的“初始值”诺擅,這是一個(gè)延遲加載的方法,只有在調(diào)用get的時(shí)候啡直,才會(huì)觸發(fā)烁涌。
- 當(dāng)線程第一次使用get方法訪問(wèn)變量時(shí)苍碟,將調(diào)用此方法,除非線程先前調(diào)用了set方法撮执,在這** 種情況下微峰,不會(huì)為線程調(diào)用本initialValue方法
- 通常,每個(gè)線程最多調(diào)用一次此方法抒钱,但如果調(diào)用了remove()后蜓肆,再調(diào)用get(),則可以再次調(diào)用此方法
- 如果不重寫(xiě)本方法,這個(gè)方法會(huì)返回null谋币。
void set(T t)
設(shè)置當(dāng)前線程的threadLocal變量為指定值.
get
返回當(dāng)前線程的這個(gè)threadLocal變量的映射值. 如果變量沒(méi)有當(dāng)前線程的值,它第一次初始化的值是由initialValue方法的調(diào)用返回
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
remove 移除當(dāng)前線程的threadLocal值
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4. 原理仗扬、源碼分析
- 每個(gè)Thread對(duì)象都持有一個(gè)ThreadLocalMap成員變量
- ThreadLocalMap存放ThreadLocal對(duì)象為key,initialValue對(duì)象或者set對(duì)象為value
- ThreadLocalMap存多個(gè)ThreadLocal
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
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;
}
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap的底層源碼分析
ThreadLocalMap是ThreadLocal內(nèi)部的一個(gè)Map實(shí)現(xiàn)蕾额,然而它沒(méi)有實(shí)現(xiàn)任何集合的接口規(guī)范早芭,因?yàn)樗鼉H供ThreadLocal內(nèi)部使用,數(shù)據(jù)結(jié)構(gòu)采用數(shù)組+開(kāi)方地址法(線性探測(cè)法)诅蝶,Entry繼承WeakRefrence退个,是基于ThreadLocal這種特殊場(chǎng)景實(shí)現(xiàn)的Map,它的實(shí)現(xiàn)方式很值得我們?nèi)⊙芯浚?/p>
ThreadLocalMap中Entry的源碼
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
源碼分析:
1.Entry中key只能是ThreadLocal對(duì)象调炬,被規(guī)定死了的
2.Entry繼承了WeakRefrence(弱引用语盈,生存周期只能活到下次GC前),但是只有Key是弱引用缰泡,Value并不是弱引用
ps:value既然不是弱引用刀荒,那么key在被回收之后(key=null)Value并沒(méi)有被回收,如果當(dāng)前線程被回收了那還好匀谣,這樣value也和線程一起被回收了照棋,要是當(dāng)前線程是線程池這樣的環(huán)境资溃,線程結(jié)束沒(méi)有銷毀回收武翎,那么Value永遠(yuǎn)不會(huì)被回收,當(dāng)存在大量這樣的value的時(shí)候溶锭,就會(huì)產(chǎn)生內(nèi)存泄漏宝恶,那么Java 8中如何解決這個(gè)問(wèn)題的呢?
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
以上是ThreadLocalMap的set方法趴捅,for循環(huán)遍歷整個(gè)Entry數(shù)組垫毙,遇到key=null的就會(huì)替換,這樣就不存在value內(nèi)存泄漏的問(wèn)題了拱绑!
ThreaLocalMap中key的HashCode計(jì)算
ThreadLocalMap這里采用的是線性探測(cè)法综芥,也就是如果發(fā)生沖突,就繼續(xù)找下一個(gè)空位置猎拨,而不是用鏈表拉鏈法膀藐。
ThreaLocalMap的key是ThreaLocal屠阻,它不會(huì)傳統(tǒng)的調(diào)用ThreadLocal的hashcode方法(繼承自object的hashcode)宿崭,而是調(diào)用nexthashcode
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
//1640531527 這是一個(gè)神奇的數(shù)字穿香,能夠讓hash槽位分布相當(dāng)均勻
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計(jì)算槽位
// hash沖突時(shí),使用開(kāi)放地址法
// 因?yàn)楠?dú)特和hash算法又憨,導(dǎo)致hash沖突很少虾啦,一般不會(huì)走進(jìn)這個(gè)for循環(huán)
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // key 相同麻诀,則覆蓋value
e.value = value;
return;
}
if (k == null) { // key = null,說(shuō)明 key 已經(jīng)被回收了傲醉,進(jìn)入替換方法
replaceStaleEntry(key, value, i);
return;
}
}
// 新增 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過(guò)期的值蝇闭,并判斷是否需要擴(kuò)容
rehash(); // 擴(kuò)容
}
源碼分析:
1.先是計(jì)算槽位
2.Entry數(shù)組中存在需要插入的key,直接替換即可需频,存在key=null丁眼,也是替換(可以避免value內(nèi)存泄漏)
3.Entry數(shù)組中不存在需要插入的key,也沒(méi)有key=null昭殉,新增一個(gè)Entry苞七,然后判斷一下需不需要擴(kuò)容和清除過(guò)期的值
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key) // 無(wú)hash沖突情況
return e;
else
return getEntryAfterMiss(key, i, e); // 有hash沖突情況
}
1.計(jì)算槽位i,判斷table[i]是否有目標(biāo)key挪丢,沒(méi)有(hahs沖突了)則進(jìn)入getEntryAfterMiss方法
5. 注意事項(xiàng)
如何避免內(nèi)存泄露
調(diào)用remove方法蹂风,就會(huì)刪除對(duì)應(yīng)的Entry對(duì)象,可以避免內(nèi)存泄露乾蓬,所以使用完ThreadLocal之后惠啄,應(yīng)該主動(dòng)調(diào)用remove方法。
空指針異常
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
public long get() {
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
System.out.println(threadLocalNPE.get());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println(threadLocalNPE.get());
}
});
thread1.start();
}
}
Exception in thread "main" java.lang.NullPointerException
at com.kpioneer.thread.threadlocal.ThreadLocalNPE.get(ThreadLocalNPE.java:15)
at com.kpioneer.thread.threadlocal.ThreadLocalNPE.main(ThreadLocalNPE.java:20)
因?yàn)閘ong是基本類型任内,這里threadLocalNPE.get()返回值null撵渡,且類型是Long為包裝類型,在拆箱過(guò)程中轉(zhuǎn)化為long類型死嗦,報(bào)空指針錯(cuò)誤趋距。
共享對(duì)象
如果在每個(gè)線程中ThreadLocal.set()進(jìn)去的東西本來(lái)就是多線程共享的同一個(gè)對(duì)象,比如static對(duì)象越除,那么多個(gè)線程的ThreadLocal.get()取得的還是這個(gè)共享對(duì)象本身节腐,還是有并發(fā)訪問(wèn)問(wèn)題。
6. 其它
- 如果可以不使用ThreadLocal就解決問(wèn)題摘盆,那么不要強(qiáng)行使用
例如在任務(wù)數(shù)很少時(shí)翼雀,在局部變量中可以新建對(duì)象就可以解決問(wèn)題,那么就不需要使用到ThreadLocal - 優(yōu)先使用框架的支持孩擂,而不是自己創(chuàng)造
例如在Spring中狼渊,如果可以使用RequestContextHolder,那么就不需要自己維護(hù)ThreadLocal类垦,因?yàn)樽约嚎赡軙?huì)忘記調(diào)用remove()方法等狈邑,造成內(nèi)存泄露坦弟。