ThreadLocal的使用场景以及使用时要注意的问题
ThreadLocal的定义
ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,每个线程只能访问和修改自己的局部变量,变量彼此之间是相互隔离的,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
ThreadLocal的使用
创建、设值、取值、删除
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("<----------"+count+"--------->");
System.out.println("第"+count+"次循环开始,当前线程中的局部变量值为:"+threadLocal.get());
threadLocal.remove();//当不做清除时候循环开始时获取的那个值是上次线程存储的,所以与多线程进行使用时注
使用场景
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection
上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
使用demo
@Test
public void testThreadLocalTwo(){
for (int i = 0; i < 3; i++) {//启动三个线程
Thread t = new Thread() {
@Override
public void run() {
add10();
}
};
t.start();
}
}
static ThreadLocal<Integer> numThreadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
/**
* 线程本地存储变量加 5
*/
private static void add10() {
for (int i = 0; i < 5; i++) {
Integer n = numThreadLocal.get();
++n;
numThreadLocal.set(n);
System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
}
}
Thread-2 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-0 : ThreadLocal num=1
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=2
Thread-1 : ThreadLocal num=3
Thread-0 : ThreadLocal num=2
Thread-1 : ThreadLocal num=4
Thread-2 : ThreadLocal num=3
Thread-1 : ThreadLocal num=5
Thread-0 : ThreadLocal num=3
Thread-2 : ThreadLocal num=4
Thread-0 : ThreadLocal num=4
最终结果可以看出来每个线程变量值的改变是互不影响的
ThreadLocal实现原理
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。 因为一个线程内可以存在多个 ThreadLocal
对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是
ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set()
方法其实都是调用了这个ThreadLocalMap类对应的 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);
}
内存泄露
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal
的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个
ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的
value。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为
null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove()
方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
为什么ThreadLocalMap的key是弱引用呢?
key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set, get,remove的时候会被清除。
解决方案
我们在使用完之后及时调用remove方法
ThreadLoca和多线程使用时注意的问题
demo
static ExecutorService executorService = Executors.newSingleThreadExecutor();
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
@Test
public void testThreadLocal(){
for(int i=0;i<5;i++){
final int count = i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("第"+count+"次循环开始,当前线程中的局部变量值为:"+threadLocal.get());
threadLocal.set("<----------"+count+"--------->");
System.out.println("第"+count+"次循环结束,当前线程中的局部变量值为:"+threadLocal.get());
// threadLocal.remove();//当不做清除时候循环开始时获取的那个值是上次线程存储的,所以与多线程进行使用时注意要remove()掉
System.out.println("当前线程名字为:"+Thread.currentThread().getName());
}
});
}
}
打印结果
第0次循环开始,当前线程中的局部变量值为:null
第0次循环结束,当前线程中的局部变量值为:<----------0--------->
当前线程名字为:pool-1-thread-1
第1次循环开始,当前线程中的局部变量值为:<----------0--------->
第1次循环结束,当前线程中的局部变量值为:<----------1--------->
当前线程名字为:pool-1-thread-1
第2次循环开始,当前线程中的局部变量值为:<----------1--------->
第2次循环结束,当前线程中的局部变量值为:<----------2--------->
当前线程名字为:pool-1-thread-1
第3次循环开始,当前线程中的局部变量值为:<----------2--------->
第3次循环结束,当前线程中的局部变量值为:<----------3--------->
当前线程名字为:pool-1-thread-1
第4次循环开始,当前线程中的局部变量值为:<----------3--------->
第4次循环结束,当前线程中的局部变量值为:<----------4--------->
当前线程名字为:pool-1-thread-1
从打印结果可以看出后面线程在第一次获取的变量值得时候获取的是上一次线程中设置的变量值,原因就在于线程池的线程复用造成的,就是上一次的线程使用完变量没有及时做清理导致的,
解决方案也很简单 就是每次使用完之后及时清理, 调用threadLocal.remove();
第0次循环开始,当前线程中的局部变量值为:null
第0次循环结束,当前线程中的局部变量值为:<----------0--------->
当前线程名字为:pool-1-thread-1
第1次循环开始,当前线程中的局部变量值为:null
第1次循环结束,当前线程中的局部变量值为:<----------1--------->
当前线程名字为:pool-1-thread-1
第2次循环开始,当前线程中的局部变量值为:null
第2次循环结束,当前线程中的局部变量值为:<----------2--------->
当前线程名字为:pool-1-thread-1
第3次循环开始,当前线程中的局部变量值为:null
第3次循环结束,当前线程中的局部变量值为:<----------3--------->
当前线程名字为:pool-1-thread-1
第4次循环开始,当前线程中的局部变量值为:null
第4次循环结束,当前线程中的局部变量值为:<----------4--------->
当前线程名字为:pool-1-thread-1
这是清除之后的打印结果,及时清理之后获取的值就是正常的