11.ThreadLocal
关于ThreadLocal主要参考了如下几篇文章,对原文作者的部分观点也提出了自己的一些看法,欢迎大家共同讨论。
ThreadLocal趣谈 —— 杨过和他的四个冤家
一个故事讲明白线程的私家领地:ThreadLocal
彻底理解ThreadLocal
1.ThreadLocal是干嘛的
ThreadLocal最核心的作用是将变量与当前线程绑定。
假设我们想在dao层拿到当前登录的用户id,我们会怎么做呢?我们可以在controller从session中获得当前用户id,然后一路传递。
@Controller
public void foo(HttpServletRequest request){
//从request中获得session,再从session获得用户id
String id = "";
//调用service的方法,将id传过去
service.foo(id);
}
@Service
public void foo(String id){
dao.foo(id);
}
@Dao
public void foo(String id){
//终于拿到了id
}
这么做一来是麻烦,万一后面还需要别的参数,那对应的方法全得改。二来可能有些方法你没源码,根本改不了。
当然,你可能会想到使用静态变量,随用随取,这样就不用通过参数一路传递了。
public class Util{
private static String id = "";
//get set方法...
}
@Dao
public void foo(){
//直接取到id
System.out.println(Util.getId());
}
这种做法很明显你将面临线程安全问题,你的整个系统不止一个人在用,id一会儿是张三,一会儿又变成李四。可变的静态变量是有大概率出现线程安全问题的,这个一定要小心。
面对这种需求,ThreadLocal就可以大显神威了。我们定义一个静态公用的ThreadLocal对象,每个线程都直接调用set和get方法,都只会取到当前线程对应的变量,相互之间不会影响。
public class ThreadUtil {
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
}
@Controller
public void foo(HttpServletRequest request){
//从request中获得session,再从session获得用户id
String id = "";
//将id与当前线程绑定
ThreadUtil.threadLocal.set(id);
}
@Service
public void foo(){
dao.foo();
}
@Dao
public void foo(){
//获取当前线程绑定的id
String id = ThreadUtil.threadLocal.get();
}
2.ThreadLocal的实现原理
要想正确明白地使用ThreadLocal,一定要看它的源码,弄清楚它到底是如何实现的。
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 getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
可以看到,set方法会先获取到当前线程,然后获取当前线程对象中,一个名为threadLocals的ThreadLocalMap类型的map,然后把自己,也就是threadLocal作为key,把要存储的值作为value,塞入这个map。可以看看Bridge4You公号总结的图,简单明了,为了更好理解,对原图有一点小改动。
ThreadLocal
图中,黄色背景表示属性,白色背景表示属性对应的具体对象。从图中可以看出,Thread中有一个名为threadLocals的属性,该属性也是一个map结构,类型为ThreadLocalMap,key为我们定义的ThreadLocal对象,value为我们调用set方法放入的值。
以上面放用户id为例,如果我们还想放用户名进去,如果继续调用set("张三"),将覆盖之前的用户id,因为key是相同的嘛。这种情况下,我们就要再创建一个ThreadLocal对象了,拿来放name,结合上面的结构图,仔细分析内部的存储方式,应该不难理解。
3.ThreadLocal线程安全?
很多文章都会说到ThreadLocal的另一个作用是可以保证线程安全,因为直接将变量与当前线程绑定了,不就变成单线程了吗,所以线程安全,乍一看貌似有道理。其实ThreadLocal的作用只是将变量与当前线程绑定,至于是不是线程安全,完全取决于你放入其中的对象是否是共享的,我们以很多文章在讲到ThreadLocal的实际应用时,都会提到典型的线程不安全类SimpleDateFormat为例。
这样SimpleDateFormat是线程安全的
public class Foo
{
// SimpleDateFormat is not thread-safe, so give one to each thread
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat("yyyyMMdd HHmm");
}
};
public String formatIt(Date date)
{
return formatter.get().format(date);
}
}
这样不是线程安全的
public class Foo
{
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmm");
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue()
{
return sdf;
}
};
public String formatIt(Date date)
{
return formatter.get().format(date);
}
}
验证是否线程安全的main方法
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(4);
for(;;){
threadPool.submit(new Runnable() {
@Override
public void run() {
try {
new Foo().formatIt(new Date(new Random().nextLong()));
} catch (Exception e) {
e.printStackTrace();
threadPool.shutdown();
}
}
});
}
}
上面那种写法是线程安全的。下面那种是不安全的,因为给每个线程绑定的还是同一个SimpleDateFormat,仍然是多线程操作同一个未进行同步处理的共享对象。有很多文章都说ThreadLocal中的value存放的是变量的副本,相互之间不会影响,但根据这个例子来看,并不是这样。
4.要清理ThreadLocal吗?
网上有很多文章都说使用ThreadLocal的时候,要注意在set之后,调用remove清理放入ThreadLocalMap的对象,否则会导致内存泄漏。我个人认为,是否需要调用remove,应该结合当时的使用场景来看。我认为关于内存泄漏,需要考虑以下几点:
1.ThreadLocal对象的数量,因为最终会作为Map的key,如果数量够大,就会导致ThreadLocalMap有很多的key-value,可能导致内存泄漏
2.线程池中的线程的数量,因为线程池中的线程是与整个应用同寿命的,如果创建海量的线程,虽然每个线程里ThreadLocalMap中放的东西不多,但你线程够多的话是有可能会有内存泄漏问题的
3.Map中的value使用频率,如果需要长期使用,且不会占用很多内存,可以考虑初始化时候set一次,而不调用remove方法
4.如果不在线程池的环境下,当然这种情况在正式项目中很少,ThreadLocal中的对象也会随着Thread的消亡而消亡,相当于会自动remove,出现内存泄漏的情况会更少
以上面的需要在dao层获得当前用户的id为例,我定义了一个静态的ThreadLocal对象,这样保证了key固定,每次进入controller都会重新设置新的value,但Map中一直都只有一个key-value。所以在该场景下我即使不调用remove清理,也不会出现任何内存泄漏的问题。
再来看看SimpleDateFormat的例子,静态的ThreadLocal,value为每次都new出来的SimpleDateFormat(),且这一个简单的对象能占10k内存了不得了,一般一个系统的时间格式都是统一的,所以反复remove set反而没必要。在使用线程池的情况下,每个线程都会一直拥有一个独立的SimpleDateFormat对象,既保证了线程安全,代码也显得简洁。
ThreadLocal的Key使用了弱引用,最大限度防止内存泄漏问题,关于这点这里就不详讲了,具体可以参考顶部引用的文章。
综上所述,关于ThreadLocal,它只负责将变量与当前线程进行绑定,至于是否线程安全,完全看你往里面放啥。如果想正确使用ThreadLocal,那就去理解它的实现,结合自己的场景来分析吧。