什么是ThreadLocal
TheadLocal可以称为线程本地变量,是一个线程内部的存储类,可以在指定的线程内存储数据,数据存储之后,只有指定的线程才能得到存储的数据。
ThreadLocal是除了加锁这种同步方式之外的一种保证线程安全的手段,通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
ThreadLocal两大常用场景
ThreadLocal在日常使用时,主要有两大应用场景:
- 每个线程需要一个独享的对象:
假设有100个线程都需要用到SimpleDateFormat类来处理日期格式
,如果共用一个SimpleDateFormat,就会出现线程安全问题,导致数据出错,如果加锁,就会降低性能,此时使用ThreadLocal,给每个线程保存一份自己的本地SimpleDateFormat,就可以同时保证线程安全和性能需求。示例代码:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 利用ThreadLocal给每个线程分配自己的dateFormat的对象,保证了线程安全和减少内存使用
*/
public class ThreadNormalUsage {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
/**
* 用来转换日期的方法
*/
public String date(int seconds) {
// 参数的单位是毫秒,从1970.1.1 00:00:00开始计时
Date date = new Date(1000 * seconds);
// 从ThreadLocal中获取dateFormat
SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return simpleDateFormat.format(date);
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadNormalUsage().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
- 每个线程内部保存全局变量,避免传参麻烦:假设一个线程的作用是拿到前端用户信息,逐层执行Service1,2,3,4层的业务逻辑,其中每个业务层都会用到用户信息,此时一个解决办法就是将user信息对象作为参数蹭蹭传递,但是这样会导致代码冗余且不利于维护。此时可以将user信息对象放入当前线程的threadlocal中,就变成了全局变量,在每一层业务层中,需要使用的时候直接从threadlocal中获取即可。
ThreadLocal源码简要分析
为什ThreadLocal是线程隔离的呢?其实在Thread类的源码中,可以找到Thread类的一个成员变量:threadlocals
。
属于这个线程的ThreadLocal值。 这个映射由ThreadLocal类维护。
可以看出,每一个线程中都会有一个属于自己的ThreadLocal值,也就是说每new出来一个线程,内部就会有一个threadlocals
。多个线程之间的threadlocals相互隔离与独立。
那么这个ThreadLocalMap是什么?其实就是ThreadLocal的本体,承载数据的一个数据结构类。根据注释的指引,来到ThreadLocal的源码中来看:
属于这个线程的ThreadLocal值。 这个映射由ThreadLocal类维护。
ThreadLocalMap是一个定制的哈希映射,只适用于维护线程本地值。 没有操作被导出到ThreadLocal类之外。
这个类是包私有的,允许在类线程中声明字段。 为了帮助处理非常大的和长期存在的使用,哈希表条目对键使用WeakReferences。
但是,由于不使用引用队列,因此只有当表空间开始耗尽时,才保证删除陈旧的条目。
ThreadLocalMap类中有一个子类Entry,就是Map中的节点/槽。在这里其继承了弱引用类
,以此来保证Entry的key是用弱引用来连接变量和对象的。
这个哈希映射中的条目扩展了WeakReference,使用其主ref字段作为键(始终是一个ThreadLocal对象)。
注意,空键(即entry.get() == null)意味着不再引用该键,因此可以从表中删除该条目。
在下面的代码中,这样的条目被称为“陈旧条目”。
在ThreadLocalMap的set()方法源码中可以看出来,当我们向ThreadLocal中set值的时候,会调用ThreadLocalMap中的set方法,而其本质是new一个Entry放进了数组中:
所以整个ThreadLocal的简单工作原理如下图所示:
ThreadLocal中的弱引用与内存泄漏问题
那么为什么ThreadLocalMap中,对key的引用使用了一种弱引用的方式呢?其主要原因是为了降低内存泄漏的风险,假设使用强引用,其结构如下:
如果此时,由于某些原因,我们将tl
置为null:tl = null
,此时就会引发内存泄漏:
如果使用弱引用,就可以保证tl对象会被及时回收掉:
那么使用了弱引用,就一定没有内存泄漏了么?答案是不一定,首先弱引用只保证了tl对象被及时回收,但还有一个value对象,可能会发生内存泄漏,当key被回收之后,如果value没有被断开引用,就会导致我们再也访问不到value,GC也无法回收value,发生内存泄漏:
不过JVM开发者也考虑到了这个问题,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。
但这样也并不能保证ThreadLocal不会发生内存泄漏,例如:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
- 分配使用了ThreadLocal又不再调用get()、set()、remove()方法,那么就会导致内存泄漏。