1.threadLocal
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。
每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用, 其它 Thread 不可访问 , 那就不存在多线程间共享的问题。这也是 ThreadLocal 命名的由来。
public static void main(String[] args) {//初始化ThreadLocal对象的时候,会重写方法initialValue(),这样就不会发生get方法返回值为null的情况。ThreadLocal<Integer> value = ThreadLocal.withInitial(() -> {return 0;});new Thread(() -> {value.set(10);//Thread-0 10System.out.println(Thread.currentThread().getName() + " " + value.get());}).start();new Thread(() -> {//Thread-1 0System.out.println(Thread.currentThread().getName() + " " + value.get());value.set(3);//Thread-1 3System.out.println(Thread.currentThread().getName() + " " + value.get());}).start();}
2.底层原理
ThreadLocal内有ThreadLocalMap内部类。
一个线程内可以存在多个 ThreadLocal 对象,每个线程都有一个ThreadLocalMap,保存了当前自己所在线程的所有“局部变量”。
set方法
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程实例
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//以当前ThreadLocal实例对象为key,传入的value值为value
else
createMap(t, value);//
}
//获取当前线程实例的threadLocals属性,保存了当前自己所在线程的所有局部变量。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread类中声明的threadLocals变量
ThreadLocal.ThreadLocalMap threadLocals = null;
//创建一个ThreadLocalMap实例对象,并以当前ThreadLocal实例对象为key,传入的value值为value
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get方法
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取当前线程所维护的ThreadLocalMap实例
if (map != null) {
//以当前ThreadLocal对象实例为key从ThreadLocalMap中获取所需要的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**初始化一个ThreadLocalMap,并以当前ThreadLocal实例对象为key,
null值为value存到这个ThreadLocalMap中,同时返回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;
}
//此方法可能被重写
protected T initialValue() {
return null;
}
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap类
ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//初始数组,长度INITIAL_CAPACITY=16
//计算当前第一个元素应该所处的位置,根据ThreadLocal的不可变实例变量 threadLocalHashCode (作为hash值)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);//直接放置当前键值对
size = 1; //元素个数
setThreshold(INITIAL_CAPACITY); //初始化扩容阈值
}
//设置对table数组进行扩容的阈值。
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
getEntry方法
//ThreadLocalMap的方法,根据key(threadlocal实例)获取entry
private Entry getEntry(ThreadLocal<?> key) {
//根据hash值确定元素所处位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//没有hash冲突,并且当前entry没有过期,一次就找到
if (e != null && e.get() == key)
return e;
else
//hash冲突或则该位置[e.get()==null(表示entry已过期)]
return getEntryAfterMiss(key, i, e);
}
//参数一:当前ThreadLocal实例
//参数二:当前ThreadLocal实例根据hash值计算出来的位置i
//参数三:位置i上的entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//循环进入条件是e!=null,为什么不考虑hash冲突,e==null,且当前查找的元素在后面?
//答:因为每次清除过期entry的时候,或则remove的时候都进行了数据修正归位置,将后面的数据移到正确的位置上去
while (e != null) {
ThreadLocal<?> k = e.get();
//是当前要找的entry
if (k == key)
return e;
//遍历到的entry已经过期,则对该位置进行探测式整理
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
//继续迭代
e = tab[i];
}
return null;
}
set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//e!=null, 遍历(可能hash冲突)
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) {
//替换过期的entry为新的键值对entry
replaceStaleEntry(key, value, i);
return;
}
}
//没有hash冲突,直接放在位置i
tab[i] = new Entry(key, value);
int sz = ++size;
//清理过期元素,检查容量
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
remove方法
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)]) {
//找到键值对entry
if (e.get() == key) {
//清除弱引用(this.referent = null; 置为过期),及entry.get()会等于null
e.clear();
//从当前位置i(当前位置的e.get()==null),开始整理元素(清理过期的,移动未过期的到正确位置)
expungeStaleEntry(i);
return;
}
}
}
replaceStaleEntry方法
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//从已经确定过期位置向前查找过期元素,以便于扩大清理范围
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)){
if (e.get() == null){
slotToExpunge = i;
}
}
//向后遍历(staleSlot+1开始),查找该元素是否存在
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//该键值对已经存在,直接替换value
if (k == key) {
e.value = value;
//将过期元素往后放(i)
tab[i] = tab[staleSlot];
//将当前entry往前放(归位)
tab[staleSlot] = e;
//除了位置staleSlot位置,未找到其他位置过期,则将开始清理位置改为i(缩小清理范围)
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//该方法会返回清理到的null的位置
int nullIndex = expungeStaleEntry(slotToExpunge);
//从nullIndex的下一个位置,简单清理几次(len=2^x,即清理x+1此)
cleanSomeSlots(nullIndex, len);
return;
}
//位置entry过期,若之前未扫描到其他过期的entry,重置过期扫描起始位置为i
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//走到这里:遍历没有找到当前键值对entry,则将元素放在当前过期的位置(替换)
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//进行到这里:先确定遍历有没有扫描到其他过期的entry
if (slotToExpunge != staleSlot){
//清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
}
cleanSomeSlots方法
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
//清理过期entry
if (e != null && e.get() == null) {
n = len;
removed = true;
//探测式清理
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
//确定是否有清楚成功
return removed;
}
expungeStaleEntry方法
该方法从一个过期的元素位置开始,并向后移动扫描,遇到过期元素就清除,遇到还处于正常状态的元素对其进行rehash操作,将正常元素移动到正确的位置上去,直到遇到null位置停止扫描,并返回null位置的索引下标。
//参数:staleSlot 该位置的entry已经过期
//核心方法,探测式整理(清理加归位处理)
//扫描从staleSlot位置开始的entry,直到遇到第一个null为止,[及staleSlot与null之间的元素]
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//清理这个过期的元素
tab[staleSlot].value = null;
tab[staleSlot] = null;
//元素个数维护
size--;
// Rehash until we encounter null
//清理过期的元素,及移动元素到正确的位置上去
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//过期的entry直接清除
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//没过期的entry进行rehash,归位
int h = k.threadLocalHashCode & (len - 1);
//元素不在正确的位置上(有hash冲突)
if (h != i) {
//置空当前位置i
tab[i] = null;
//从正确位置开始查找(新位置可能hash冲突),找到空位
while (tab[h] != null)
h = nextIndex(h, len);
//新的位置
tab[h] = e;
}
}
}
return i;
}
初始情况如图

假设调用expungeStaleEntry时staleSlot=3

清除位置i=3处的元素:

继续向后扫描

发现i=4处的元素正常,并且 int h = k.threadLocalHashCode & (len - 1);得出h=3,说明加入该元素时有hash冲突,需要对该元素归位到位置3处。

继续向后扫描到i=5,发现该元素过期,直接清除

继续向后扫描i=6,发现该元素E6正常,并且 int h = k.threadLocalHashCode & (len - 1); 得出h=3,需要移动该元素,但是i=3处已经有元素,所以需要往3之后放置到i=4处。

继续向后扫描i=7,发现该元素正常,并且 int h = k.threadLocalHashCode & (len - 1); 得出h=7,不需要移动

继续向后扫描 i=8,发现该位置为null,退出循环,扫描结束,返回该位置的位置索引
3.内存泄漏
原因
每一个Thread维护一个ThreadLocalMap映射表,映射表的key是ThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。
如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。
解决
每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
为什么设计弱引用
假设map中的引用是强引用,那么由于map中依然有这个对象的引用,那么这个对象不能够被GC回收。
子线程中获取主线程的值
主线程中创建InheritableThreadLocal实例对象,ThreadLocal threadLocal = new InheritableThreadLocal()。
原理
InheritableThreadLocal继承了ThreadLocal,并且重写了childValue、getMap和createMap方法。
通过new Thread()方式创建子线程时,进入如下代码。
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 如果父线程的inheritableThreadLocals不为null
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 通过createInheritedMap方法将父线程中inheritableThreadLocals的值复制到子线程的inheritableThreadLocals中
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}
