多线程访问共享可变数据时,涉及到线程间的数据同步的问题,并不是所有时候都需要共享数据,因此,线程封闭的概念就出来了。
线程封闭:数据被封闭在各自的线程中,不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭,线程封闭具体的实现有ThreadLocal、局部变量
局部变量:局部变量的固有属性就是封闭在线程中,位于执行线程的栈中,其他线程无法访问
ThreadLocal 使用
使用场景:
- 每个线程都需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random)
- 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
ThreadLocal 的作用:
- ThreadLocal 可以让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
- 在任何方法中都可以轻松的获取到该对象
根据共享对象的生成时机不同,选择 initialValue 或 set 来保存对象:
- initialValue:在 ThreadLocal 第一次 get 的时候就可以把对象给初始化出来,即对象的初始化时机可以由我们控制,如 SimpleDateFormatter
- set:需要保存到 ThreadLocal 中的对象生成时机不由我们随意控制,如拦截器生成的用户信息,用 ThreadLocal.set 直接放入到 ThreadLocal 中去,以便后续使用
ThreadLocal 的优势:
- 实现线程安全
- 不需要加锁,提高执行效率
- 更加高效的利用内存、节省开销
- 避免传参的麻烦,降低耦合度
场景一
场景一:每个线程需要一个独享的对象,每个 Thread 内有自己的实例副本,不共享,如 SimpleDateFormat 的使用,使用 ThreadLocal.withInitial(Suppiles) 方法进行初始化
public class ThreadLocalUsageOne {
public static String date(int seconds) {
Date date = new Date(1000 * seconds);
// 每个线程都有自己唯一的一个SimpleDateFormatter
return ThreadSafeFormatter.dateFormatThreadLocal.get().format(date);
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
service.execute(() -> {
System.out.println(ThreadLocalUsageOne.date(finalI));
});
}
service.shutdown();
}
}
// 通过该类获取每个线程独有的SimpleDateFormatter
public class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
// lambda写法
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal1 = ThreadLocal.withInitial(() -> {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});
}
场景二
场景二:当前用户信息需要被线程内所有方法共享,这些信息存在如下特点:
- 在同一线程内,当前用户信息(如用户名、user ID一致)是相同的,需要被该线程内的所有方法共享
- 不同的业务线程中的用户信息是不同的
在该场景下,常用的解决方法:
1、一种解决方法是将 user 作为参数层层传递,从 service-1 一直传递下去,该方案会导致代码冗余和不易维护
2、另一种解决思路是在每个线程内保存全局变量,可以在不同的方法直接使用,避免参数传递的麻烦,在这种思路,可以有如下两种实现
- 使用一个 UserMap,缺点是多线程同时工作时,为了保证线程安全,需要使用 synchronized 或者 ConcurrentHashMap,会影响性能
- 使用 ThreadLocal ,通过 ThreadLocal.set() 与 ThreadLocal.get() 在同一线程内存取
```java
public class ThreadLocalUsageTwo {
public static void main(String[] args) {
} }ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(()->{
String name = Thread.currentThread().getName()+ "-" + finalI;
new Service1().process(name);
});
}
executorService.shutdown();
class User { private String name;
public User setName(String name) {
this.name = name;
return this;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
// 用于生成用户类 class Service1 { public void process(String name) { // 创建用户类 User user = new User().setName(name); // 将用户类放入ThreadLocal UserContextHolder.holder.set(user); new Service2().process(); } }
// 用于持有用户类
class UserContextHolder {
public static ThreadLocal
// 在其他服务中使用 class Service2 { public void process() { User user = UserContextHolder.holder.get(); System.out.println(“当前线程:” + Thread.currentThread().getName() + “\t” + user); } }
<a name="LC50j"></a>
#### ThreadLocal 原理
Thread、ThreadLocal、ThreadLocalMap 的关系:
- Thread 类中存在一个成员变量 **ThreadLocalMap** ,用于存储 ThreadLocal
- 每一个 ThreadLocalMap 中,存在许多的 ThreadLocal
![屏幕截图 2020-11-17 184018.png](https://cdn.nlark.com/yuque/0/2020/png/750131/1605609737393-b6566dbe-251a-494f-96db-7463ae15c088.png#align=left&display=inline&height=847&margin=%5Bobject%20Object%5D&name=%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202020-11-17%20184018.png&originHeight=847&originWidth=953&size=211398&status=done&style=none&width=953)
ThreadLocal 的重要方法:
- T initialValue():初始化
- 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用 get() 方法的时候,才会被触发
- 当线程第一次使用 get 方法访问变量时,将会调用此方法,除非线程先前调用了 set 方法,在这种情况下,不会为线程调用 initialValue 方法
- 每个线程最多调用一次此方法,但如果已经调用了 remove 后,再调用 get,则可以再次调用此方法
- 如果不重写此方法,这个方法会返回 null,因此,一般使用匿名内部类的方法来重写 initialValue 方法,以便在后续使用中可以初始化副本对象
- void set(T t):为这个线程设置一个新值
- T get():得到这个线程对应的value,如果首次调用 get,则会调用 initialize 来得到这个值
- void remove():删除对应这个线程的值
set 和 get 方法源码解析:
- get:先取出当前线程的 ThreadLocalMap,然后调用 map.getEntry 方法,将本 ThreadLocal 的引用作为参数传入,取出 map 中属于本 ThreadLocal 的 value
- 注意:这个map及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
```java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap 类,也就是 Thread.threadLocals,ThreadLocalMap 类是每个线程 Thread 类中的变量,里面最重要的是一个键值对数组 Entry[] table,其中
- 键:这个 ThreadLocal
- 值:实际需要的成员变量,如 user 或 simpleDateFormatter 对象
ThreadLocal 可能存在的问题
内存泄漏:某个对象不再有用,但占用的内存却不能被回收
弱引用:如果这个对象只被弱引用关联(没有被任何强引用关联),那么这个对象就可以被回收,即弱引用不会阻止GC
ThreadLocal 可能的内存泄露:ThreadLocalMap 中的 key 和 value
- key:ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,当 key 不再使用时,该引用不会阻止 key 的回收,因此 key 不会造成内存泄漏
- value:ThreadLocalMap 的每个 Entry 都包含一个对 value 的强引用
- 正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被因为没有任何强引用而被垃圾回收
- 当线程不终止(即线程需要保持很久,如使用线程池),那么 key 对应的 value 就不能回收,因为有如下的调用链:Thread -> ThreadLocalMap -> Entry (key 为 null) -> value,即 value 和 Thread 之间存在强引用链路,导致 value 无法回收
- 针对上述情况,jdk 已经做了处理,即在 set、remove、rehash 这些常用方法中会扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就可以被回收
- 然而如果一个 ThreadLocal 不被使用,那么 set、remove、rehash 方法也不会被调用,如果线程又不停止,那么调用链路就会一直存在,导致 value 的内存泄露,因此在使用完 ThreadLocal 后,一定要调用 remove 方法,删除对应的 Entry 对象,避免内存泄漏
其他 ThreadLocal 的使用问题:
- 空指针异常:ThreadLocal 在 get 之前不需要先进行 set,因为直接 get 会返回 null,而不会抛出空指针异常
- 共享对象:如果每个线程中 ThreadLocal.set 进去的东西本来就是多线程共享的一个对象,比如 static 对象,那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,还是存在并发访问控制问题,因此不要往 ThreadLocal 中存入共享变量
- 当线程数很少的时候,如果在局部变量中新建对象可以解决问题,就不需要强行使用 ThreadLocal
- 优先使用框架的支持,而不是自己创造:如在 spring 中,如果可以使用 RequestContextHolder,那就不需要自己维护 ThreadLocal,免得忘了调用 remove 而导致内存泄漏
Spring 中 ThreadLocal 的使用
- DateTimeContextHolder 类
- RequestContextHolder 类
- 每次 HTTP 请求都对应一个线程,线程之间相互隔离,这就是 ThreadLocal 的典型应用