简述ThreadLocal 使用和原理
在线程的调用中可以通过ThreadLocal进行的参数传输,减少方法中的参数一层层的嵌套下去,而且它是线程安全的,通过使用ThreadLocal 的方式可以在代码和多线程的情况下处理很多线程安全的问题。
那它是怎样做到在线程里面里面做到线程安全的呢?
那它是通过怎样的格式保存数据的呢?
存在多数据的时候是否会导致内存泄露呢?
接下来将探讨下ThreadLocal底层的实现
简单 main 方法示例:
public class TreadLocalDemo {
private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
threadLocal1.set("11111");
threadLocal2.set("22222");
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
threadLocal2.remove();
}).start();
}
}
得到结果:

这里可以看到 通过set方法就可以 在同一个线程下面每个地方可以通过获取到threadLocal 的引用就可以通过Get 方法获取到set进去的值.
上面只是简单示范下,你学会后可以有各种各样的变体。
这里是Set时候的源码
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
这里可以看到通过获取到当前Thread 然后获取之前是否已经存在了ThreadLocalMap 对象;
如果当前线程没有ThreadLocalMap 就会创建一个,并且把引用设置会Thread 对象里面,创建ThreadLocalMap 时初始化一个默认值为16的 Entry[] ,并且Entry对应的key就是当前ThreadLocal的引用,而且是个弱应用(后面会描述他们的关系),value就是set的值。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 创建一个默认长度等于16的数组
table = new Entry[INITIAL_CAPACITY];
// 通过当前ThreadLocal hashcode值 和 当前的容量 取 &
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 设置值
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 是这里计算下一次扩展临界值长度
setThreshold(INITIAL_CAPACITY);
}
关于 firstKey.threadLocalHashCode 计算方式可以参考: https://zhuanlan.zhihu.com/p/40515974
如果当前对象已经存在ThreadLocalMap的话
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
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)]) {
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();
}
这里代码的大体已经就是通过计算出当前threadLocal的索引值 然后把值设置到对应的ThreadLocalMap的Entry对应的数组中。
这里描述ThreadLocal 、ThreadLocalMap、Entry[]、Entry 它们的关系:
一个Thread,里面只有一个THreadLocalMap,在ThreadLocalMap 里面所以会有一个Entry[],而且在创建Entry对象的时候 Key是threadLcoal、value 是设置的值
上图可以说明:
如果我们当前Thread 执行完成后就会断开强引用,只有一个弱引用,这样弱引用对象来说就会被垃圾回收机制给回收,防止内存溢出。
难道这样就不会产生内存溢出吗?假如设置 threadLocal = null ,而且当前Thread还存活这时候。Entry对象里面的Key是弱应用他会不会被回收 ? 不会(因为它本身其实是线程本身强引用,只有当不存在线程的强引用之后,这个weakreference才会在GC的时候被端),但是这时候Entry 对象里面的 key = null 这样下来不一样会存在不断的内存占用吗? 一样有可能导致内存的溢出。
当然上面说的设计者也想到了,所以分别在执行set、get、remove的方式里面都会尝试或者直接去检查key = null 的值去移除。(这里并不是所有的set、get操作都会去执行,remove 是先设置成null然后去移除 可以去看看源码里面的这个方法 expungeStaleEntry )
也许你留意到我上面的main方法的代码里面的ThreadLocal是一个静态的对象,如果你把他加上一个final 的话 ,上面是不是就成为一个伪命题了。。。具体实现Thread 写法很多。但是建议最后还是我们通过手动remove() 方法进行移除。
可能有人会问为什么需要把存储对应格式设计成数组,那你可以看看我一开始的main方法,因为一个Thread里面可以持有一个或者多个以上的ThreadLcoal的引用,这样的话就需要数组来进行存储了。
举例使用场景:
1、spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如 RequestContextHolder、 TransactionSynchronizationManager、 LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理让它们也成为线程安全的状态。 2、PageHelper找那个startPage就是通过ThreadLocal来进行保存当前分页信息。
这里只是本人对ThreadLocal一些简单的见解,如果有错误之处请谅解,望指出,谢谢!!!