要理解 ThreadLocal,先来看看官方对它的解释:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
我翻译一下,大概意思是这样(可能翻译的不是很准确,见谅):
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
总结一下重点:
- ThreadLocal 提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是当前这个线程私有的,这就是所谓的线程隔离。
- 如果要使用 ThreadLocal,通常定义为 private static 类型,在我看来最好是定义为 private static final 类型。
理解了 ThreadLocal,我们来看 ThreadLocal 的使用场景:
对同一个线程调用的多个方法中,共享了某一个变量,这个变量需要传递到多个方法中,这样传来传去太麻烦了,这时就可以采用 ThreadLocal 了。
下面我们来看一个错误的用法。先创建一个用来作为共享变量的实体类:
package com.xttblog.test; public class Xttblog { private String title; private Long id; private String text; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getText() { return text; } public void setText(String text) { this.text = text; } public Xttblog(String title, Long id, String text) { this.title = title; this.id = id; this.text = text; } }
然后创建一个线程 ThreadLocalTarget:
package com.xttblog.test; public class ThreadLocalTarget implements Runnable{ private static Xttblog blog = new Xttblog("ThreadLocal 会导致内存泄露?",1L,"...."); public static ThreadLocal<Xttblog> local = new ThreadLocal<Xttblog>(); public ThreadLocalTarget(){} @Override public void run() { ThreadLocalTarget.local.set(blog); ThreadLocalTarget.local.get().setTitle("test"); System.out.println(Thread.currentThread().getName() + "更改后的名字:" + ThreadLocalTarget.local.get().getTitle()); } public static Xttblog getBlog() { return blog; } }
然后模仿这个线程在实际中的应用:
package com.xttblog.test; public class ThreadLocalTest { public static void main(String[] args) throws InterruptedException { ThreadLocalTarget target = new ThreadLocalTarget(); Thread thread = new Thread(target); thread.start(); thread.join(); System.out.println(Thread.currentThread().getName() + "更改后的名字:" + ThreadLocalTarget.getBlog().getTitle()); } }
运行 ThreadLocalTest 之后,你会傻眼了。ThreadLocal 不是共享变量,是拷贝的变量的一个副本吗?怎么变量的值最终被改变了。
Thread-0更改后的名字:test main更改后的名字:test
这不是 ThreadLocal 的错,是你没理解她。
我前面说过 ThreadLocal 保证的是同一个线程内部调用的各种方法共享当前线程中 ThreadLocal 的变量。就是说针对同一个线程中任何地方访问属于当前现在的共享变量是同一个。从上面也可以看出 ThreadLocal 不是拷贝的变量的副本。
还没明白(也许是我没说清楚),我们来看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:
package com.xttblog.test; import java.text.DateFormat; import java.text.SimpleDateFormat; public class DateUtil { public static final ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; }
然后我们再要用到 DateFormat 对象的地方,这样调用:
DateUtils.df.get().format(new Date());
如果还没明白,我再解释一下,ThreadLocal 相当于每个线程A在创建的时候,已经为你创建好了一个 DateFormat,这个 DateFormat 在当前这个线程A中共享。其他线程B,再用到 DateFormat 的地方,也会创建一个 DateFormat 对象,这个对象会在线程 B 中共享,直到线程 B 结束。
也就是说 ThreadLocal 的用法和我们自己 new 对象一样,然后将这个 new 的对象传递到各个方法中。但是到处传递的话,太麻烦了。这个时候,就应该用 ThreadLocal。
总结:ThreadLocal 并不是为了解决线程安全问题,而是提供了一种将实例绑定到当前线程的机制,类似于隔离的效果,实际上自己在方法中 new 出来变量也能达到类似的效果。ThreadLocal 跟线程安全基本不搭边,绑定上去的实例也不是多线程公用的,而是每个线程 new 一份,这个实例肯定不是共用的,如果共用了,那就会引发线程安全问题。ThreadLocal 最大的用处就是用来把实例变量共享成全局变量,在程序的任何方法中都可以访问到该实例变量而已。网上很多人说 ThreadLocal 是解决了线程安全问题,其实是望文生义,两者不是同类问题。