概述
并发问题,有时分,能够用ThreadLocal办法来防止。
ThreadLocal,望文生义,便是线程自己的,独享的,就像线程栈是线程独享的相同。
本文评论三点:
- 基本用法
- 规划原理
- 父子线程
基础用法
考虑类A有doSync办法,可能会被并发调用. 由于SimpleDateFormat非线程安全,所以在办法内new创立。
public void doSync(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
simpleDateFormat.format(new Date());
// .... some complex ops
}
能够优化为以下方案,让每个线程都有自己的SimpleDateFormat, 然后不用每次调用都new一个:
private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public void doSync(){
SimpleDateFormat simpleDateFormat = sdf.get();
simpleDateFormat.format(new Date());
// .... some complex ops
}
阐明:本段代码可能在某个类A中, 办法doSync可能会被并发调用。
如果在doSync内部new一个SimpleDateFormat,同一个线程调用也要每次都new一个,有损功能,其实同一个线程能够共享一个。所以,能够用一个ThreadLocal类型的变量,包括一个SimpleDateFormat。 这里没有调用remove,是希望每个线程里都常驻一个日期格式化目标。
另外的一个栗子是,咱们在web开发里,有时分会跨层传达一些上下文信息,会运用ThreadLocal,譬如在某个filter里运用set办法设置,然后完毕的时分remove。
ThreadLocal主要办法阐明:
- withInitial : 承受一个Supplier(函数接口,界说了get办法,望文生义,便是供给者),供给什么?当然是供给要放在ThreadLocal内的变量,由于是要在线程内创立,不是立刻要,所以需求的是一个supplier
- set: 设置当时线程ThreadLocal包括的值
- get: 获取当时线程ThreadLocal包括的值
- remove:移除当时线程ThreadLocal包括的值
规划原理
为了愈加直观的感受ThreadLocal和ThreadLocal所容纳变量的联系,能够继续看下图。 ThreadLocal仅仅是一个访问者,线程独占的变量在各自线程的ThreadLocalMap中。
不过需求留意的是,图中,ThreadLocal目标T1自身的引证,有目标A,线程1,线程2,线程3一共4个持有者。
咱们仍是用上文日期格式化的代码来阐明对应联系:
// 类A的代码片段
private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public void doSync(){
SimpleDateFormat simpleDateFormat = sdf.get();
simpleDateFormat.format(new Date());
// .... some extremely complex ops
}
- 变量X: 便是 ThreadLocal.withInitial 里边 Supplier办法的返回值,一个SimpleDateFormat目标
- ThreadLocal目标T1: 便是类A里界说的成员变量 ThreadLocal sdf
- ThreadLocalMap: 一种Map数据结构,类似HashMap,线程结构里自己完成一个Map,应该是不想和调集结构耦合吧
问题1: 为什么要用一个Map呢?
由于这种A目标可能有很多个,变量X,ThreadLocal目标T1都会有很多个。
问题2: 有人说ThreadLocal有内存走漏,是什么意思?
首先咱们清晰一下内存走漏: 不会再运用的目标或许变量,占用着内存,且无法被GC掉,称为内存走漏。 ThreadLocal在线程的ThreadLocalMap中,Key是ThreadLocal目标, Value是变量X副本,走漏的可能是Key和Value。
事例中的日期格式化东西,仅仅在A的代码片段里有用,而当A目标GC-Root不可达要被干掉了,ThreadLocal目标T1的强引证sdf就没有了,而线程1,2,3里的各自ThreadLocalMap中还有。当不规范运用的时分,或许便是顽强,不remove。一朝一夕,就会有很多无用的Key和Value充满着ThreadLocalMap。
可是呢,顽强的我回想了一下,其实往往都没事,这么久了,我都没删啊,也没遇到走漏啊!
作为结构规划者自然会考虑到,为了便利这些上帝运用结构(Java程序员),从2点分别针对Key和value的走漏:
- 运用ThreadLocal弱引证作为Key,当ThreadLocal变量只要弱引证时,就会被GC掉,ThreadLocalMap里的key就会指向null(或许说Key便是null)
- ThreadLocalMap当rehash的时分,会干掉key为null对应的Value (这或许也是自己完成一个Map的原因吧)
所以,如果没有rehash,走漏仍是存在的,只不过,一般很难达到觉察的程度。
下面,从源码的角度佐证一下针对走漏所做的2个要点。
弱引证:
// ThreadLocal.ThreadLocalMap.Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // ThreadLocal k 在这里开始被弱引证指向了
value = v;
}
}
清理key为null的value:
// ThreadLocal.ThreadLocalMap.rehash
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
// ThreadLocal.ThreadLocalMap.resize
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
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();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
父子线程
有时分,执行事务逻辑需求异步,可是当时线程的ThreadLocal变量,怎样传递给子线程呢?
ThreadLocal有个子类InheritableThreadLocal, 基本运用如下:
static class A {
private InheritableThreadLocal<HashMap<String, String>> map1 = new InheritableThreadLocal<HashMap<String, String>>(){
@Override
protected HashMap<String, String> initialValue() {
return new HashMap<>(8);
}
};
private ThreadLocal<HashMap<String, String>> map2 = new ThreadLocal<HashMap<String, String>>(){
@Override
protected HashMap<String, String> initialValue() {
return new HashMap<>(8);
}
};
public void doAsync(){
map1.get().put("name", "zhangsan");
map2.get().put("name", "zhangsan");
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("map1: " + map1.get().get("name")); // 子线程t能够读取到map1的name
System.out.println("map2: " + map2.get().get("name")); // 却无法读取到map2的name
}
}, "A-SUB-0");
t.start();
}
}
怎样传递的呢?
创立线程的时分,Thread类的构造函数会判断当时线程中是否存在InheritableThreadLocal, 如果有,就会仿制一份。
// Thread类构造函数执行的代码片段: 体现了对inheritableThreadLocal的仿制
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
仅仅是在创立线程的时分,会产生一次仿制, 仿制的是ThreadLocalMap里的Entry数组,即包括Key:ThreadLocal目标和Value目标。
- 后续父线程内有增减ThreadLocal,都和子线程无关。所以和线程池结合运用的时分,需求特别留意一下。
- Key和Value都是引证仿制,所以,同一个ThreadLocal Key和对应的Value变化,父子线程是共享的
伏笔
写到这里,发现还遗漏了一个知识点,便是ThreadLocalMap这个数据结构怎样完成的。
能够带着这几个问题去看下源码,本文暂时先留下伏笔吧~
- 怎么仅仅用一个数组来完成Map
- 怎么处理hash抵触
- 怎么扩容?