ThreadLocal含义
ThreadLocal线程本地变量把变量与线程绑定在一起,为每一个线程维护一个独立的变量副本(因为是对象引用,堆中的对象是线程间共享的,所以ThreadLocal没有解决线程安全问题),在本线程内随时可取。而ThreadLocal实例通常是private static类型的,用于关联线程。
原理
public class Thread implements Runnable {
省略
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocal 包含了静态内部类ThreadLocalMap,Thread使用了ThreadLocalMap。
ThreadLocalMap包含静态内部类Entry,还包含Entry数组(每个数组元素是ThreadLocal+value一对,threadLocal对象是弱引用,GC时自动回收)。
ThreadLocal的整体结构
引用关系
除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。
ThreadLocal对象不一定在堆上。如果ThreadLocal被定义成了static的,那么ThreadLocal对象是类共用的,可能出现在方法区。
为什么用ThreadLocal做key?
ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?
如果应用中一个线程只使用一个ThreadLocal对象,那么使用Thread做key也可以。
@Service
public class ThreadLocalService {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}
如果应用中一个线程不只使用了一个ThreadLocal对象,那么使用Thread做key有问题?
@Service
public class ThreadLocalService {
private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
}
通过Thread对象,无法知道要获取哪个ThreadLocal对象?
Entry的key为什么设计成弱引用?
假如key对ThreadLocal对象的弱引用改为强引用。
ThreadLocal变量对ThreadLocal对象是强引用。即使ThreadLocal变量设置成null,但key对ThreadLocal还是强引用。如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁,那么就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。ThreadLocal对象和ThreadLocalMap都不会被GC回收,产生了内存泄露问题。
弱引用的对象,GC时回收。即如果key是弱引用,当ThreadLocal变量指向null之后,GC时回收key,其值设置成null。
ThreadLocal变量指向null,调用它的get、set或remove方法,会出现空指针异常。如果另外一个ThreadLocal变量b调用了它的get、set或remove,触发清理机制,将key为null的value值清空。如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了,在最大程度上解决内存泄露问题。
Entry中key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有指向null,GC把弱引用的key回收了,会影响使用。如果当前ThreadLocal变量指向null,并且key也为null了,没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。
弱引用的例子
public static void main(String[] args) {
WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
System.out.println(weakReference0.get());
System.gc();
System.out.println(weakReference0.get());
}
运行结果
java.lang.Object@28d93b30
null
传入WeakReference构造方法的是直接new出来的对象,没有其他引用,在调用gc方法后,弱引用对象会回收。
public static void main(String[] args) {
Object object = new Object();
WeakReference<Object> weakReference1 = new WeakReference<>(object);
System.out.println(weakReference1.get());
System.gc();
System.out.println(weakReference1.get());
}
运行结果
java.lang.Object@28d93b30
java.lang.Object@28d93b30
先定义一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。gc后弱引用对象不会回收。
Entry对象中的key属于第2种情况。
public static void main(String[] args) {
Object object = new Object();
WeakReference<Object> weakReference1 = new WeakReference<>(object);
System.out.println(weakReference1.get());
System.gc();
System.out.println(weakReference1.get());
object=null;
System.gc();
System.out.println(weakReference1.get());
}
运行结果
java.lang.Object@28d93b30
java.lang.Object@28d93b30
null
如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。
Entry的value为什么不设计成弱引用?
如果Entry的value只是被Entry引用,没被业务系统中的其他地方引用,那么value是弱引用时GC回收后会导致系统异常。相比之下,Entry的key指向的是ThreadLocal。
ThreadLocal如何导致内存泄露?
如果ThreadLocalMap中存在很多key为null的Entry,没有调用过有效的ThreadLocal的get、set或remove方法,那么Entry的value值不会被清空,
存在一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。Entry和ThreadLocalMap会长期存在下去,会导致内存泄露。
如何解决内存泄露问题?
在使用完ThreadLocal对象之后调用它的remove方法,remove方法会把Entry中的key和value都设置成null。
public class Main {
public String get() {
try{
return CurrentUser.get();
} finally {
CurrentUser.remove();
}
}
public static void main(String[] args) {
CurrentUser.set("abc");
System.out.println(new Main().get());
System.out.println(new Main().get());
}
static class CurrentUser {
private static final ThreadLocal<String> THREA_LOCAL = new ThreadLocal<>();
public static void set(String str) {
THREA_LOCAL.set(str);
}
public static String get() {
return THREA_LOCAL.get();
}
public static void remove() {
THREA_LOCAL.remove();
}
}
}
运行结果
abc
null
ThreadLocal是如何定位数据的?
ThreadLocal的get、set、remove方法中都有这样一行代码:
int i = key.threadLocalHashCode & (len-1);
假设len=16,key.threadLocalHashCode=31
int i = 31 & 15 = 15相当于int i = 31 % 16 = 15
Entry数组长度是2^n,位运算效率更高。
1.通过key的hashCode取余计算出一个下标。
2.通过下标定位具体Entry,如果找到了,那么返回。
3.如果第2步没有找到,那么从数组的下标位置继续往后找,遇到最后一个位置时从头开始继续找。
4.直到找到第一个Entry为空为止。
ThreadLocal有哪些用途?
1.在Spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
2.获取当前登录用户上下文。
3.临时保存权限数据。
代码举例
场景:有5个线程,这5个线程都有一个值value,初始值为0,线程运行时用一个循环往value值相加数字。
public class TestThreadLocal {
private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new MyThread(i)).start();
}
}
static class MyThread implements Runnable {
private int index;
public MyThread(int index) {
this.index = index;
}
public void run() {
System.out.println("线程" + index + "的初始value:" + value.get());
for (int i = 0; i < 10; i++) {
value.set(value.get() + i);
}
System.out.println("线程" + index + "的累加value:" + value.get());
}
}
}
运行结果
线程0的初始value:0
线程3的初始value:0
线程2的初始value:0
线程2的累加value:45
线程1的初始value:0
线程3的累加value:45
线程0的累加value:45
线程1的累加value:45
线程4的初始value:0
线程4的累加value:45