ThreadLocal看这一篇就够了
关联文章:
一、HashMap看这一篇就够了
二、synchronized看这一篇就够了
三、HTTP协议看这一篇就够了
四、Java线程池看这一篇就够了
一、ThreadLocal简介
ThreadLocal名为线程变量,ThreadLocal中填充的变量属于当前线程,该变量只属于当前线程,其他线程都是无法访问到的(下面会讲解其原理)。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
二、ThreadLocal的简单使用
// new一个用于存放用户信息的ThreadLocal
private static final ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();
public Response handleRequest(UserInfo userInfo) {
Response response = new Response();
try {
// 1.用户信息set到线程局部变量中
userInfoThreadLocal.set(userInfo);
doHandle();
} finally {
// 3.业务逻辑进行完后 调用remove函数除掉threadlocal的值(非常重要)
userInfoThreadLocal.remove();
}
return response;
}
//业务逻辑处理
private void doHandle () {
// 2.实际用的时候取出来
UserInfo userInfo = userInfoThreadLocal.get();
//进行业务逻辑处理
operation(userInfo);
}
三、ThreadLocal的原理
从源码出发。
3.1 ThreadLocal的set()方法:
public void set(T value) {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取线程中的属性 threadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果threadLocalMap 不为空,则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
从上面的代码可以看出,ThreadLocal.set()赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public class ThreadLocal<T> {
// ThreadLocalMap 是ThreadLocal类下的内部静态类
static class ThreadLocalMap {
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的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用(下面会说明为什么要用弱引用)。在Entry内部使用ThreadLocal本身作为key,使用我们设置的Object作为value。
3.2 ThreadLocal的get()方法
public T get() {
// 1、获取当前线程
Thread t = Thread.currentThread();
// 2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 3、如果map数据不为空
if (map != null) {
// 获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
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;
}
3.3 ThreadLocal的remove()方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove方法,直接将ThrealLocal 对应的值从当前Thread中的ThreadLocalMap中删除。至于为啥要删除掉,这涉及到内存泄露的问题。
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
到这个时候我们可以得知,有两个引用指向着 ThreadLocal,一个是调用使用ThreadLocal对象的地方使用的是强引用,一个是ThreadLocalMap中使用的key使用的是弱引用。
为什么要设计两个引用指向ThreadLocal呢?
答:因为要尽可能的防止内存泄漏的情况出现。
在使用线程池的情况下,线程执行完成后并不会销毁,而是会回到线程池中,自然线程自带ThreadLocal也就不会被销毁。到此我们就要开始思考为什么ThreadLocalMap的key使用的是弱引用?在线程执行完成后,调用ThreadLocal的强引用就没有了,只剩下ThreadLocalMap的key指向的弱引用,那么如果在只有弱引用的情况下,在下一次GC时就会把key清理掉。如果ThreadLocalMap的key使用的是强引用,那么在未调用remove()的情况下,ThreadLocalMap的key就永远无法被清理掉,导致内存泄露的问题。
看完上面关于弱引用的解释,相信很多同学都有一个疑问,设置双引用的目的是为了清理掉key,防止内存泄露,那么只清理了key,但value未被清理不是依然会有内存泄露的问题吗?首先,这个问题是肯定的,value未被清理是会有内存泄露的问题,JDK设计者之所以还是要这样设计大概是因为要尽可能的减少内存泄漏的情况。当然彻底解决ThreadLocal内存泄露的问题的办法就是**一定要调用remove()**方法。
综上所述,在使用ThreadLocal的时候,一定要在线程执行结束时调用**remove()**方法。
3.4 java四种引入介绍
-
强引用Strong Reference
java中的引用默认就是强引用,任何一个对象的赋值操作就产生了对这个对象的强引用。
强引用的特性是只要有强引用存在,被引用的对象就不会被垃圾回收。 -
软引用Soft Reference
软引用的特性是只有在JVM内存不足的情况下,被引用的对象才会被回收。 -
弱引用weak Reference
弱引用的特性是只要GC执行,就会被回收,而不管是否内存不足。 -
虚引用PhantomReference
虚引用的作用是跟踪垃圾回收器收集对象的活动,主要是用来管理JVM外的对象回收过程。
四、ThreadlocalMap 与 HashMap的区别
首先,ThreadlocalMap跟HashMap 一样,都是数组实现的。但在于发生hash冲突时的操作是不同的,对待哈希冲突,HashMap采用的链表 + 红黑树的形式,链表长度过长(>8) 就会转成红黑树,详细可以看HashMap看这一篇就够了 这篇文章。
而ThreadlocalMap既没有链表,也没有红黑树,采用的是开放定址法,如果发生冲突,ThreadlocalMap直接往后找相邻的下一个节点,如果相邻节点为空,直接存进去,如果不为空,继续往后找,直到找到空的,把元素放进去,或者元素个数超过数组长度阈值,进行扩容。以下是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;
// hashcode & 操作其实就是 %数组长度取余数,例如:数组长度是4,hashCode % (4-1) 就找到要存放元素的数组下标
int i = key.threadLocalHashCode & (len-1);
// 找到数组的空槽(=null),一般ThreadlocalMap存放元素不会很多
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果key值一样,算是更新操作,直接替换
if (k == key) {
e.value = value;
return;
}
// key为空,做替换清理动作,这个后面聊WeakReference的时候讲
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 新new一个Entry
tab[i] = new Entry(key, value);
// 数组元素个数+1
int sz = ++size;
// 如果没清理掉元素或者存放元素个数超过数组阈值,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
五、ThreadLocal 常见使用场景
如上文所述,ThreadLocal 适用于如下两种场景
- 1、每个线程需要有自己单独的实例
- 2、实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。
场景如下:
- 存储用户Session
- 数据库连接,处理数据库事务。
java中连接数据库和处理数据库事务中都用到了ThreadLocal,这里抛出个问题~~,在平时编写业务代码时,为什么在进行跨实例操作时,事务会失效。大家可以看看源码找找原因(─.─|||。 - 数据跨层传递(controller,service, dao)
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦但不想被多线程共享(因为不同线程获取到的用户信息不一样)。 - Spring使用ThreadLocal解决线程安全问题
我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。