1.概述
ThreadLocal并不是为了解决保证多线程对共享变量的使用,而是当每个线程需要使用一个变量时,将该变量保存到当前线程中,实现了多副本保存。
这是由于每个线程都维护了一个字段ThreadLocal.ThreadLocalMap threadLocals = null;
,在这个map中,key是ThreadLocal的实例的引用,val是set方法传入的值。如果定义了多个ThreadLocal的实例对象,这个map中就会包含多个值。
2.基本使用
public class ThreadLocalTest {
static ThreadLocal localVar = new ThreadLocal<>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar1");
//调用打印方法
print("thread1");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//设置线程1中本地变量的值
localVar.set("localVar2");
//调用打印方法
print("thread2");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
});
t1.start();
t2.start();
}
}
输出结果为:
thread1 :localVar1
after remove : null
thread2 :localVar2
after remove : null
3.应用场景
数据块连接池
Spring中Dao层装配的Connection,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都使用同一个连接去连接数据库,那么就会造成线程不安全的问题。解决方法就是使用ThreadLocal,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题。
4.四大引用
强引用
是指创建一个对象并把这个对象赋给一个引用变量。强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
软引用
垃圾回收时如果内存不够就回收,否则不回收
弱引用
只要触发垃圾回收就一定会回收
虚引用
任何时候都会被回收,需要配合Reference使用
例子
public class Reference {
public static void main(String[] args) {
System.out.println("soft");
testSoft();
System.out.println("weak");
testWeak();
System.out.println("Phantom");
testPhantom();
}
static void testSoft(){
SoftReference sr = new SoftReference<>(new String[8]);
System.out.println(sr.get());;
System.gc();
System.out.println(sr.get());
}
static void testWeak(){
WeakReference sr = new WeakReference<>(new String[8]);
System.out.println(sr.get());;
System.gc();
System.out.println(sr.get());
}
static void testPhantom(){
ReferenceQueue q = new ReferenceQueue<>();
PhantomReference sr = new PhantomReference<>(new String[8],q);
System.out.println(sr.get());;
System.gc();
System.out.println(sr.get());
}
}
输出:
soft
[Ljava.lang.String;@1540e19d
[Ljava.lang.String;@1540e19d
weak
[Ljava.lang.String;@677327b6
null
Phantom
null
null
5.弱引用问题
在ThreadLocalMap中每个Entry定义如下:
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
可以看到,其中key是弱引用。其中指向关系如下
问题1.先执行set,再gc ,keynull吗(ThreadLocal对象的实例还存在吗)?
不会,设A对象中包含一个ThreadLocal对象的实例,因为A对象没有被回收,ThreadLocal对象不会被回收,key就还存在。
问题2.什么时候key为null
如果A对象被回收,ThreadLocal对象就会被回收,进行一次gc,由于key是弱引用,那么ThreadLocal对象就会被回收,key=null。
问题3.为什么key被设计为弱引用
假设key被设计为强引用,如果让ThreadLocal这个引用=null,但由于key是强引用,指向实际的ThreadLocal对象,因此ThreadLocal对象不会被回收.我们既不能访问到ThreadLocal,又不能被回收,因此发生内存泄漏.
而如果key是弱引用,当ThreadLocal这个引用=null,key是弱引用,进行一次gc会让该引用为null,然后ThreadLocal对象就会被回收.ThreadLocalMap还会根据Entry!=null && key == null 清理掉无效的value,保证Entry正常回收。
同时我们也看出,如果ThreadLocal实例如果声明成static会保证正常使用。
6.源码剖析
1.set方法
步骤一:
获取当前线程,不存在map则创建,否则调用map的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);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
步骤二:
循环往后找,如果找到一个key相等的直接替换并返回,如果找到一个待清理(key == null && Entry != null)的,执行替换replaceStaleEntry(key, value, i)
并返回,否则找到的就是空的,创建新的,执行启发式清理,如果启发式清理没有清理任何Entry且里面的Entry数量达到了数组长度的2/3,进行扩容。
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();
}
步骤三:
替换replaceStaleEntry(key, value, i)
(1)从当前位置向前查找,如果找到前面第一个待回收的,如果到了null,就结束,标记为slotToExpunge
(2)从当前位置向后查找,如果找到了和当前key相等的,替换value,并交换当前Entry[i] 和EntrystaleSlot,(更靠近了)。
如果上一步没有找到更前面的待回收的Entry,将slotToExpunge设为i
从slotToExpunge执行一次探测式清理,并将返回值返回,进行一次启发式清理,并返回.
如果往后找时找到了待回收的,更新slotToExpunge为i,因为循环外是在传入参数的位置插入的值。
(3)staleSlot插入新值,如果上面找到了其他待回收的,执行cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
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;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
2.hash算法
下标的计算是通过一个固定值&(len - 1),下一个要添加的值的下标是通过获取ThreadLocal的属性private final int threadLocalHashCode = nextHashCode();
实现的
public class ThreadLocal {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
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);
}
}
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
3.hash冲突
每次冲突后采用线性探测,循环往后找
绿色:key != null & Entry != null
灰色:key = null & Entry != null
白色: Entry != null
4.探测式清理
先直接将Entry和value置空,再探测,如果遇到null,则结束,遇到待回收的,令Entry == null,value == null,遇到正常的,判断index是否和正常计算的结果一致,不一致则从应该开始的位置遍历,如果找到null,则放入该位置.返回值为Entry == null 的位置,代表之前的位置都被我清理了.
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
5.扩容机制
在扩容前,先从头进行一次探测式清理,如果清理结束,当前的size依然大于等于threshold*3/4
,则扩容。
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
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);
}
}
扩容就是将容量扩大为2倍,然后从头遍历,对每个元素重新rehash,如果key为null,则置value为null,如果Entry != null && key != null, 则将往后找第一个为null的位置,然后插入
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;
} 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;
}
6.get方法
如果直接定位到,则返回,否则往后找,如果是待清理的则从当前位置执行一次探测式清理,清理过程中,在expungeStaleEntry
中将不会被回收的往前移动。最后返回
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
7.启发式清理
至少进行log2len次探测式清理,每次清理都是一段(不包含空),如果当中找到了待清理的对象,重新设置清理次数为log2len
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);
return removed;
}