ThreadLocal提供一个保存线程本地变量的方案。每个线程都能保存它自己的变量,线程之间变量独立。
又学习到多线程相关的了。看了下ThreadLocal的作者,又有Doug Lea大神。日常膜拜一下大神(牛逼牛逼牛逼)
强软弱虚四种应用
强引用:比如说
Student stu = new Student();
这个stu就是强引用,只要这个引用指向堆中的Student对象。Student对象就不会被垃圾回收器回收。
软引用:
SoftReference soft = new SoftReference<>(new Student());
Student student = soft.get();
soft里保存了指向Student对象的引用。通过soft.get()可以获取到Student对象。
但是当堆内存空间不足的时候,这个Student对象就会被垃圾回收器回收。用soft.get()就获取不到对象了,返回的是null
弱引用:
WeakReference weak = new WeakReference<>(new Student());
Student student = weak.get();
weak里保存了指向Student对象的引用。通过weak.get()可以获取到Student对象。
但是当垃圾回收器启动的时候,这个Student对象就会被垃圾回收器回收。用weak.get()就获取不到对象了,返回的是null
虚引用:PhantomReference
用于调度回收前的清理工作,管理堆外内存。
内存图
先来看一下内存图,看看这个类是怎么存东西的
public class Test {
public static void main(String[] args){
ThreadLocal t = new ThreadLocal<>();
t.set(new Student());
t.get();
t.remove();
}
}
class Student{}
写了一段代码,往ThreadLocal里存我们自己的一个类Student
看上面这个图。我们的student对象副本其实保存在线程对象中的,所以能够实现不同线程之间互不影响。
Thread,就是线程对象,里面有一个属性threadLocals,它的类型是ThreadLocal的一个内部类
threadLocals对象里有一个属性table,是一个Entry数组
Entry对象它是继承于WeakReference,它自己有一个属性value,这个引用指向的就是我们要保存的student对象。还有一个继承于父类的属性referent,指向的就是ThreadLocal对象。弱引用的特点是,当垃圾回收器启动时,如果没有其它应用指向这个ThreadLocal对象,仅仅只有弱引用指向该对象,那么该对象就会被回收掉。
一些要点
1.用继承于WeakReference的Entry对象存储key和value,key是弱引用,指向ThreadLocal对象。成员变量value是强引用,指向我们要保存的对象,存储在线程对象中。
2.存储键值对的时候,遇到hash冲突,不会像HashMap一样构建链表,而是往数组后面的空位存。
3.get、set、remove方法中发现key为空都会清理表中的失效项目。
4.数组扩容的时候,会先清理整个数组的失效项目,将size减小一点。如果不超过阈值了,就不需要扩容。如果还是超过阈值,才会将数组扩容2倍
源代码
set方法
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程对象
ThreadLocalMap map = getMap(t);//获取线程对象里的threadLocals
if (map != null)//如果map已经实例化了,那么往里放值
map.set(this, value);
else//如果还没有实例化,那么就去创建
createMap(t, value);
}
getMap(t)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;//返回线程对象中的属性threadLocals
}
创建map的方法,也就是创建threadLocals
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);//new 一个ThreadLocalMap
}
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//创建一个初始容量为16的Entry数组
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//和HashMap一样,也是用hash&(数组长度-1)来计算出数组下标
table[i] = new Entry(firstKey, firstValue);//创建一个Entry放到数组中
size = 1;
setThreshold(INITIAL_CAPACITY);//设置扩容阈值,这里是16*2/3
}
再来看下map.set(this, value);
private void set(ThreadLocal> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {//如果有数组该位置有对象的情况,是hash冲突了,遍历数组,往后放
ThreadLocal> k = e.get();
if (k == key) {//找到key相同的,就替换value
e.value = value;
return;
}
if (k == null) {//key为空?这是弱引用指向的ThreadLocal对象已经被回收了,代表数组这个位置的Entry已经废弃了,替换掉它
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);//找到了数组中的一个空位置,new一个Entry放进去
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)//这里是清理一些陈旧的项目。什么是陈旧的项目?就是弱引用被回收的项目,就是key已经是null了。那么要将数组这个位置清理掉
rehash();
}
看下替换陈旧项目的方法replaceStaleEntry(key, value, i);
会扫描数组,并且清理找到的陈旧项目。但是由于采用了对数次扫描,不能完全清理整个数组的陈旧项目
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {//传入的staleSlot是陈旧插槽的数组索引位置
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;//这两个值相等了,后面可能会判断到
for (int i = prevIndex(staleSlot, len);//往前找key为空的插槽。
(e = tab[i]) != null;//数组是有阈值控制扩容的,不会存满,会碰到null的情况跳出循环
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;//出循环之后slotToExpunge的位置是一个key被回收掉的Entry。后面清理会从这个位置开始往后清理。
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;//如果往后找,碰到一个位置没有Entry,就退出循环了
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {//往后找到了key相等的,替换
e.value = value;
tab[i] = tab[staleSlot];//和失效的槽替换一下。保证数组中的key都是连续的,不为空?
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)//这里相等,是因为前面往前找没有找到key为空的
slotToExpunge = i;//刚刚交换了一下,要清理的位置是从i开始了
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//清理插槽
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)//往后找,碰到一个key为空的。
slotToExpunge = i;//如果slotToExpunge == staleSlot的话,表示531行往前找没有key为空的了。后面的清理插槽就从i的位置开始吧。
}
// If key not found, put new entry in stale slot//key没有找到的话,会到这里
tab[staleSlot].value = null;//回收陈旧项目的value
tab[staleSlot] = new Entry(key, value);//创建新的键值对放入
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)//不相等是由于531的循环往前找key为空的键值对
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
对数扫描,清理一些插槽的方法cleanSomeSlots(int i, int n)
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];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//对数次扫描。这是在不扫描和线性扫描之间的平衡。后者虽然能清除所有垃圾,但是会造成某些插入操作花费O(n)的时间
return removed;
}
expungeStaleEntry用于清除传入的位置到下一个数组空位置之间的所有陈旧项目
private int expungeStaleEntry(int staleSlot) {//清除staleSlot和下一个数组中的空位置之间的所有陈旧条目
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;//将value置空了,可以回收掉了
tab[staleSlot] = null;//Entry也置空了
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;//循环,清理staleSlot和下一个数组中的空位置之间的所有陈旧项目
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == null) {//碰到key为空的,清一清
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {//不相等,表示存在hash冲突。由于之前的位置可能已经被清理,当前i位置的Entry需要移一下位置
tab[i] = null;//将数组中的引用置空。e中保存了对象
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)//从第一个hash位置开始找起,直到找到数组中的空位置
h = nextIndex(h, len);
tab[h] = e;//将Entry放入数组
}
}
}
return i;//返回的i的索引是数组中的一个空位置
}
最后看一下存储的条目超过阈值,数组需要扩容的处理
private void rehash() {
expungeStaleEntries();//清除陈旧条目,这个会清理整个数组
//清理完之后,size会变小
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)//清理之后条目还是很多,超过阈值的话,数组就真的要扩容了
resize();
}
清除整个数组的陈旧条目的方法expungeStaleEntries()
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {//遍历整个数组
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);//这个方法?上面见过
}
}
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)//hash冲突的时候往后面的位置放
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);//设置新阈值
size = count;
table = newTab;
}
get方法
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取线程对象的threadLocals
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//用ThreadLocal从map中获取Entry对象
if (e != null) {//有对象的话,返回Entry中的value,就是我们保存的对象。
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//设置初始值
}
看下map.getEntry(this)
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);//计算直接下标
Entry e = table[i];
if (e != null && e.get() == key)//如果直接计算出的下标能找到key对应的,就返回
return e;
else//为什么会找不到呢?因为存储方式和HashMap不同,这个是往数组后面的空位置存的,而不是构建一个链表
return getEntryAfterMiss(key, i, e);
}
看下getEntryAfterMiss(key, i, e)
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//往后找,直到碰到数组的位置为空了还没找到,就是真没有了
while (e != null) {//e不为空的话,遍历数组找key对应的
ThreadLocal> k = e.get();
if (k == key)//找到key相等的,返回
return e;
if (k == null)//key为空,清除陈旧条目
expungeStaleEntry(i);
else
i = nextIndex(i, len);//计算出下一个索引
e = tab[i];
}
return null;//如果e为空的话,那就是没有存入对象
}
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
看下m.remove(this);
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)]) {//遍历数组找
if (e.get() == key) {//找到key相等的了
e.clear();//将指向key的应用referent置空
expungeStaleEntry(i);//清除陈旧条目
return;
}
}
}
一些思考
1.我们在使用的时候,用完了对象一定要记得用remove()方法清理一下。
虽然Entry保存的key是弱引用,当我们不用ThreadLocal的时候,对象就会被回收。key是被回收了,但是value是一个强引用,不会被回收,可能就会造成内存泄漏的危险。
2.为什么有了remove()方法做清理了,还要设计成弱引用呢?
我觉得这是为了兜底。假如我们忘记了remove,仅仅把我们用到的ThreadLocal--A置为null了,我们代码中可能还有其它的ThreadLocal--B,调用B的get,set,remove方法的时候,也会对ThreadLocalMap里的数组进行废弃项的清除。那什么是废弃的呢,key是null的就是废弃的,弱引用保证了key可以变成null。这样设计最大程度减少了内存泄漏的可能性。
最可怕的是:我们用了线程池中的线程,用完ThreadLocal之后忘记了remove,只将ThreadLocal回收了。而线程一直存活在线程池中,那个Entry里的value所指向的对象也一直回收不掉。
3.initialValue()方法
在ThreadLocal源代码中,这个方法是返回null的。这个方法需要我们自己创建子类去覆写,return我们自己需要的对象。
protected T initialValue() {
return null;
}
一开始没有设置值,就去get的话,就会调用这个方法,往ThreadLocal里设值。remove之后再调用get方法,也会去调用initialValue方法设值。