多线程访问同一个共享变量的时候容易出现并发问题,ThreadLocal是除了加锁外的一种规避多线程不安全的方法。
ThreadLocal是JDK包提供的,它提供线程本地变量,每个线程都会有变量的一个副本,访问的都是线程自己的变量副本,从而规避了线程安全问题,如下图所示
public class ThreadLocalTest {
static ThreadLocal<String> 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
}
}
每个线程都维护了一个ThreadLocalMap,而这个map的key就是threadLocal,value就是set存储的对象。
set方法
//Entry为ThreadLocalMap静态内部类,对ThreadLocal的若引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//位运算,结果与取模相同,计算出需要存放的位置
//threadLocalHashCode比较有趣
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
实例化ThreadLocalMap时创建了一个长度为16的Entry数组。每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,可以理解成每个线程Thread都持有一个Entry型的数组table。
通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。
//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();
一个Thread来说只有持有一个ThreadLocalMap,所以ABC三个ThreadLocal对应同一个ThreadLocalMap对象。
为了管理ABC,通过将threadLocal的hashCode进行位运算(取模)得到索引i,将ABC存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。
总结如下:
ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
在数据库连接上的应用
内存溢出: Memory overflow 没有足够的内存提供申请者使用.
内存泄漏: Memory Leak 程序中已经动态分配的堆内存由于某种原因, 程序未释放或者无法释放, 造成系统内部的浪费, 导致程序运行速度减缓甚至系统崩溃等严重结果。内存泄漏的堆积终将导致内存溢出。
假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了。
但是因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal),造成ThreadLocal无法被回收。
在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
假设在业务代码中使用完ThreadLoca, ThreadLocal ref被回收了。
由于threadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例(这里Entry不再强引用ThreadLocal了), 所以threadlocal就可以顺利被gc回收, 此时Entry中的key = null。
在没有手动删除Entry以及CurrentThread依然运行的前提下,也存在始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry,value就不会被回收,而这块value永远不会被访问到了(因为key=null),导致value内存泄漏。
两种内存泄漏的情况中.都有两个前提:
1 . 没有手动侧除这个 Entry
2 . CurrentThread 当前线程依然运行
综上, ThreadLocal 内存泄漏的根源是:
由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除(remove()方法)对应 key 就会导致内存泄漏。
要避免内存泄漏有两种方式:
那么为什么 key 要用弱引用呢
事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的。
这就意味着使用完 ThreadLocal,CurrentThread 依然运行的前提下。就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收。对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏。
参考:
ThreadLocal:https://www.jianshu.com/p/3c5d7f09dfbd
Java中的ThreadLocal详解:https://www.cnblogs.com/fsmly/p/11020641.html
ThreadLocal 内存泄露问题:https://blog.csdn.net/JH39456194/article/details/107304997