一、ThreadLocal使用场景
Java为保证并发情况下的线程安全,提供了多种可用的方案,典型的如:各种锁(包括synchronize关键字)、原子类、ThreadLocal类等。前面对锁以及原子类已经进行了较为详细的介绍,这里继续介绍ThreadLocal。
ThreadLocal 有两种典型的使用场景:
- 场景1:用作保存每个线程独享的对象,为每个线程都创建一个副本,每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
- 场景2:用作每个线程内需要独立保存的信息,以便供线程内其他方法更方便地获取该信息。线程内前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念
1. 用作线程独享的对象
以这样一个场景为例:多个线程,要求每个线程打印的时间不重复,这里需要借助SimpleDateFormat类,该类是一个线程不安全的工具类。
如果线程数为1000,这些线程共享一个SimpleDateFormat对象,由于SimpleDateFormat类是线程不安全的,为了保证线程安全,必然要加上锁。
public class ThreadLocalDemo01 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
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 ThreadLocalDemo01().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalDemo01.class) {
s = dateFormat.format(date);
}
return s;
}
}
执行结果如下:
00:00
00:01
00:06
...
15:56
16:37
16:36
打印结果满足要求,但是这样的效率会非常低。首先,1000个线程的创建和销毁需要耗费大量资源;其次,由于SimpleDateFormat线程不安全而加上synchronized 关键字,还会带来同步的开销。
那么继续简化一下,对于创建过多线程带来的开销,采用之前提到过的线程池可以很好地降低,其内部的线程复用,最终创建的线程数会远远低于1000个;对于同步的开销,是由于多个线程共享一个SimpleDateFormat对象造成的,如果让每个线程都拥有一个自己的 simpleDateFormat 对象,就不会存在线程之间的竞争,也就不需要同步。
public class ThreadLocalDemo02 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
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 ThreadLocalDemo02().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
return dateFormat.format(date);
}
}
打印结果如下:
00:00
00:07
00:04
00:02
...
16:29
16:28
16:27
16:26
16:39
打印的结果同样满足要求。
上面的案例里,通过让每个线程都拥有一个自己的 simpleDateFormat 对象来保证线程安全,这其实就是ThreadLocal的解决方案。同样采用上面的优化方案,这次用ThreadLocal来进行解决
public class ThreadLocalDemo03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
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 ThreadLocalDemo03().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
};
}
这里ThreadLocal 会去帮每个线程去生成它自己的 simpleDateFormat 对象。
2. 用作线程内独立保存的信息
在实际的业务场景中,有一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。这些业务内容在不同的方法中都有使用到,这种情况下,一般的做法是将这些业务内容层层传递给需要使用的方法。
例如,service-1()、service-2()、service-3()、service-4()代表不同的业务处理逻辑,这些方法都需要用户信息User,就需要把这个 user 对象层层传递下去,从 service-1() 传到 service-2(),再从 service-2() 传到 service-3()。
这样的做法会造成代码冗余,如果使用一个静态的Map,在执行 service-1() 的时候,把用户信息给 put 进去,后面需要拿用户信息的时候,直接从静态的 User map 里面 get 就可以了。同时考虑到HashMap不够安全,可以使用 synchronized,或者直接把 HashMap 替换成 ConcurrentHashMap来保证线程安全,如下所示:
ThreadLocal的作用(实际上应该说是ThreadLocalMap)就类似于上面提到的静态的Map,但是与上面采用加锁或者ConcurrentHashMap的cas操作来保证线程安全不同,ThreadLocal为每一个线程单独生成User对象,从而避免了线程之间的竞争,保证了线程安全。
具体的使用方法如下:
public class ThreadLocalDemo07 {
public static void main(String[] args) {
new Service1().service1();
}
}
class Service1 {
public void service1() {
User user = new User("test");
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2 {
public void service2() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().service3();
}
}
class Service3 {
public void service3() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = n
}
}
运行结果如下:
Service2拿到用户名:test
Service3拿到用户名:test
二、ThreadLocal vs Thread vs ThreadLocalMap
ThreadLocal、Thread、ThreadLocalMap这三者之间的关系密不可分,具体的关系可以参考以下示意图:
总结一下,一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。
具体分析源码如下,先看ThreadLocal的getMap 方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// Thread的成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;
Thread 中有一个名为threadLocals 的成员变量,初始值为null,而它就是ThreadLocalMap 类型,ThreadLocal的getMap方法就是返回给定Thread的成员变量threadLocals 。
再看ThreadLocal的set方法
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 ;如果 map == null 则去创建这个 map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。
在最后再看ThreadLocalMap ,以下是自定义在 ThreadLocal 类中的 ThreadLocalMap 类:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
//...
}
ThreadLocalMap 类是每个线程 Thread 类里面的一个成员变量,在 ThreadLocalMap 中会有一个 Entry 类型的数组,名字叫 table。我们可以把 Entry 理解为一个 map,其键值对为:
- 键,当前的 ThreadLocal;
- 值,实际需要存储的变量,比如 user 用户对象或者 simpleDateFormat 对象等。
ThreadLocalMap 类似于HashMap ,但是与HashMap 不同的是,HashMap 在面对 hash 冲突的时候,采用的是拉链法。它会先把对象 hash 到一个对应的格子中,如果有冲突就用链表的形式往下链;ThreadLocalMap 解决 hash 冲突采用的是线性探测法。如果发生冲突,并不会用链表的形式往下链,而是会继续寻找下一个空的格子。
三、ThreadLocal的相关问题
1. ThreadLocal能否解决共享资源的多线程访问
先说结论,不能!
ThreadLocal确实可以用于解决多线程情况下的线程安全问题,但其资源并不是共享的,而是每个线程独享的。
ThreadLocal 解决线程安全问题的时候,相比于使用“锁”而言,换了一个思路,把资源变成了各线程独享的资源,非常巧妙地避免了同步操作。具体而言,它可以在 initialValue 中 new 出自己线程独享的资源,而多个线程之间,它们所访问的对象本身是不共享的,自然就不存在任何并发问题。这是 ThreadLocal 解决并发问题的最主要思路。
如果把放到 ThreadLocal 中的资源用 static 修饰,让它变成一个共享资源的话,那么即便使用了 ThreadLocal,同样也会有线程安全问题。
public class ThreadLocalStatic {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
static SimpleDateFormat dateFormat = new SimpleDateFormat("mm:ss");
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 ThreadLocalStatic().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal <SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal <SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return ThreadLocalStatic.dateFormat;
}
}
}
执行结果如下:
00:15
00:15
00:05
00:16
...
打印结果中00:15 被多次打印了,发生了线程安全问题。如果我们需要放到 ThreadLocal 中的这个对象是共享的,是被 static 修饰的,那么此时其实根本就不需要用到 ThreadLocal,即使用了 ThreadLocal 并不能解决线程安全问题。如果想要保证它的线程安全,应该用其他的方法,比如说可以使用 synchronized 或者是加锁等其他的方法来解决线程安全问题。
2. ThreadLocal vs synchronized
前面提到过,ThreadLocal和synchronized都能保证线程安全,两者之间的区别是啥呢?
这要从ThreadLocal的两个应用场景出发
首先是ThreadLocal用作线程独享的对象,通过让每个线程独享自己的副本,避免了资源的竞争从而保证了线程安全。而synchronized 内部基于monitor锁,限制在同一时刻最多只有一个线程能访问资源,从而保证了线程安全。相比于 ThreadLocal 而言,synchronized 的效率会更低一些,但是花费的内存也更少。
其次是ThreadLocal用作线程内独立保存的信息,这种场景更注重的效果是避免传参,此时 ThreadLocal 和 synchronized 是两个不同维度的工具。
3. 内存泄露
内存泄露,是指当某一个对象不再有用的时候,占用的内存却不能被回收。
在使用ThreadLocal时就会面临内存泄露的风险
a. Key的泄露
之前提到过,每个Thread内部都有一个ThreadLocalMap。线程在访问了 ThreadLocal 之后,都会在它的 ThreadLocalMap 里面的 Entry 中去维护该 ThreadLocal 变量与具体实例的映射。
当业务代码不再使用某个ThreadLocal时,执行ThreadLocal instance = null 操作,但是ThreadLocalMap 的 Entry 中还引用着这个ThreadLocal示例。也就是说,虽然在业务代码中把 ThreadLocal 实例置为了 null,但是在 Thread 类中依然有这个引用链的存在。这就会存在内存泄露的风险。
ThreadLocalMap为了避免这种内存泄露风险,其内部的Entry 继承了 WeakReference 弱引用,对Key是一个弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用的特点是,如果这个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,这样就不会因为Entry 中还引用着ThreadLocal示例,而导致无法GC回收无用的ThreadLocal示例。
b. Value的泄露
上面提到ThreadLocalMap内部的Entry 继承了 WeakReference 弱引用,它对Key是弱引用,但是它对Value却是强引用。上面的代码中,value = v就是强引用的体现。
正常情况下,当线程结束,Value就没有了任何强引用,此时可以正常垃圾回收掉。但是如果线程迟迟不结束,而Value早就不再使用,由于Entry的强引用,Value不会被垃圾回收掉。
如图所示,显示的是线程内的引用关系,对于Value,引用链路为:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例。
为了解决这个问题,ThreadLocal 设计在执行set、remove、rehash 等方法时,会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。
c. 内存泄露如何避免
上面提到了Key和Value两种情况的内存泄露,而Key引起的内存泄露由于引入了弱引用,已经得到了解决。但是对于Value的泄露,需要及时的调用set、remove、rehash 等方法。
也就是说,为了避免Value引起的内存泄露,在使用完了 ThreadLocal 之后,应该手动去调用它的 remove 方法。
查看一下remove的源码
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
首先获取线程的ThreadLocalMap,再调用ThreadLocalMap的remove方法,将key 所对应的 value 给清理掉,这样一来,value 就可以被 GC 回收了。