ThreadLocal特性及使用场景
1、方便同一个线程使用某一对象,避免不必要的参数传递;
2、线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);
3、获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);
ThreadLocal应注意:
1、ThreadLocal并未解决多线程访问共享对象的问题;
2、ThreadLocal并不是每个线程拷贝一个对象,而是直接new(新建)一个;
3、如果ThreadLocal.set()的对象是多线程共享的,那么还是涉及并发问题。
ThreadLocal示例:
public class Demo {
//声明ThreadLocal并初始化赋值
private static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(){
@Override
protected Object initialValue()
{
Map<String,Object> map = new HashMap<>();
map.put("a","a");
map.put("b","b");
map.put("c","c");
return map;
}
};
public static void main(String[] args){
//线程1 - 对其获取并修改
new Thread(() -> {
Object stringObjectMap = threadLocal.get();
System.out.println("什么1:" + stringObjectMap + " -> " + Thread.currentThread().getName());
//赋值
Map<String,Object> map = new HashMap<>();
map.put("d","1");
map.put("e","2");
map.put("f","3");
threadLocal.set(map);
Object stringObjectMap2 = threadLocal.get();
System.out.println("什么2:" + stringObjectMap2 + " -> " + Thread.currentThread().getName());
}).start();
//线程2 - 对其获取 - 为了检测,加了睡眠1秒的代码
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object stringObjectMap = threadLocal.get();
System.out.println("什么3:" + stringObjectMap + " -> " + Thread.currentThread().getName());
}).start();
}
}
//输出
什么1:{a=a, b=b, c=c} -> Thread-0 - 归属线程1
什么2:{d=1, e=2, f=3} -> Thread-0 - 归属线程1
什么3:{a=a, b=b, c=c} -> Thread-1 - 归属线程2
从结果可以看出,虽然两个线程共同使用一个ThreadLocal,但两个线程中所展示的ThreadLocal的数据值
并不会相互影响,也就是说这种情况下的threadLocal变量保存的数据相当于是线程安全的,只能被当前线程访问。
ThreadLocal源码
set()
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap,返回map
ThreadLocalMap map = getMap(t);
if (map != null)
//将数据放入ThreadLocalMap中,key是当前ThreadLocal对象,值是我们传入的value。
map.set(this, value);
else
//map为空,初始化ThreadLocalMap,并以当前ThreadLocal对象为Key,value为值存入map中。
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set的代码逻辑比较简单,主要是把值设置到当前线程的一个ThreadLocalMap对象中,而ThreadLocalMap可以理解成一个Map,它是定义在Thread类中内部的成员,初始化是为null。
//它在Thread类中
ThreadLocal.ThreadLocalMap threadLocals = null;
与常见的Map实现类,如HashMap之类的不同的是,
ThreadLocalMap中的Entry是继承于WeakReference类的,
保持了对 “键” 的弱引用和对 “值” 的强引用,
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);//调用弱引用构造
value = v;
}
}
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);
}
}
ThreadLocalMap是一种Map,其内部维护着一个Entry[]。
ThreadLocalMap其实是就是将Key和Value包装成Entry,然后放入Entry数组中。看一下它的set方法。
Entry构造函数中的参数 k 就是ThreadLocal实例,调用super(k) 表明对 k 是弱引用,
使用弱引用的原因在于,当没有强引用指向 ThreadLocal 实例时,它可被回收,从而避免内存泄露。
当调用set方法时,其实是将数据写入threadLocals这个Map对象中,
这个Map的key为ThreadLocal当前对象,value就是我们存入的值。
而threadLocals本身能保存多个ThreadLocal对象,相当于一个ThreadLocal集合。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//通过ThreadLocal的threadLocalHashCode与当前Map的长度计算出数组下标 i
int i = key.threadLocalHashCode & (len-1);
//从i开始遍历Entry数组,这会有三种情况:
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//1、Entry的key就是我们要set的ThreadLocal,直接替换Entry中的value。
if (k == key) {
//如果已经存在,直接替换value
e.value = value;
return;
}
//2、Entry的key为空,直接替换key和value。
if (k == null) {
//如果当前位置的key ThreadLocal为空,替换key和value。
//下文ThreadLocal内存分析中会提到为什么会有这段代码。
replaceStaleEntry(key, value, i);
return;
}
//3、发生了Hash冲突,当前位置已经有了数据,查找下一个可用空间。
}
//找到没有数据的位置,将key和value放入。
tab[i] = new Entry(key, value);//该位置没有数据,直接存入。
int sz = ++size;
//检查是否扩容。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
应该可以看出ThreadLocalMap就是一种HashMap。
不过它并没有采用java.util.HashMap中数组+链表的方式解决Hash冲突,而是采用index后移的方式。
HashMap是一种get、set都非常高效的集合,它的时间复杂度只有O(1)。
但是如果存在严重的Hash冲突,那HashMap的效率就会降低很多。
我们通过上段代码知道,ThreadLocalMap是通过 key.threadLocalHashCode & (len-1)
计算Entry存放index的。len是当前Entry[]的长度,这没什么好说的。
那看来秘密就在threadLocalHashCode中了。我们来看一下threadLocalHashCode是如何产生的。
public class ThreadLocal<T> {
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);
}
}
这段代码非常简单。有个全局的计数器nextHashCode,
每有一个ThreadLocal产生这个计数器就会加0x61c88647,
然后把当前值赋给threadLocalHashCode。什么是0x61c88647?
16进制 0x61c88647
10进制 1640531527
2进制 01100001110010001000011001000111 10011110001101110111100110111001(取反+1)
10011110001101110111100110111001对应的10进制为2654435769;
在斐波那契散列法中2654435769是一个斐波那契散列乘数,
它的优点是通过它散列出来的结果分布会比较均匀,可以很大程度上避免hash冲突;
get()
public T get() {
Thread t = Thread.currentThread();
//直接获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果该对象不为空就返回它的value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//否则为空,设置初识值到ThreadLocal中并返回
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
其实就是每个线程都维护着一个ThreadLocal的容器,这个容器就是ThreadLocalMap,可以保存
多个ThreadLocal对象。而调用ThreadLocal的set或get方法其实就是对当前线程的ThreadLocal变量操作,
与其他线程是分开的,所以才能保证线程私有,也就不存在线程安全的问题了。
然而,该方案虽然能保证线程私有,但却会占用大量的内存,因为每个线程都维护着一个Map,
当访问某个ThreadLocal变量后,线程会在自己的Map内维护该ThreadLocal变量与具体实现的映射,
如果这些映射一直存在,就表明ThreadLocal 存在引用的情况,那么系统GC就无法回收这些变量,
可能会造成内存泄露。
remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
//这就比较简单了,删除操作
ThreadLocal是怎么解决内存泄露的
重点就在ThreadLocal弱引用上,关于对象的引用查阅对象的那篇文章。
弱引用可以保证,只要GC到来,那么就必定回收它。
但是,仅仅是它的key是弱引用,value并不会回收。
ThreadLocal内存分析
我们假设ThreadLocal完成了自己的使命,与ThreadLocalRef断开了引用关系。此时内存图变成了这样。
系统GC发生时,由于Heap中的ThreadLocal只有来自key的弱引用,因此ThreadLocal内存会被回收到。
到这里,value被留在了Heap中,而我们没办法通过引用访问它。value这块内存将会持续到线程结束。
如果不想依赖线程的生命周期,那就调用remove方法来释放value的内存吧。
在面试中,使用完的话,还是要手动的写上remove方法。